fish2018/pansou · warning

转义序列不完整

Error message

转义序列不完整

What it means

decodeHexEscape parses a fixed-length run of hex digits from a JS single-quoted string's \xNN escape in the haitunsou plugin. It throws 转义序列不完整 (incomplete escape sequence) when the computed slice [start, start+length) extends past the input data, meaning the escape started but not enough bytes remain to complete it (e.g. a truncated \x at end of input).

Solutions

  1. Log the input around `start` to inspect the malformed escape before changing code
  2. Pre-validate the input string: confirm it ends with a complete escape sequence before decoding
  3. Treat the decode error as a decode failure for that item: fall back to the raw title or skip the item instead of failing the whole search
  4. If the site systematically truncates payloads, re-fetch the page or use a different endpoint

Example fix

// before
value, n, err := decodeHexEscape(data, i+2, 2)
if err != nil {
    return "", err
}
// after
value, n, err := decodeHexEscape(data, i+2, 2)
if err != nil {
    log.Printf("haitunsou: bad escape at %d, using raw title", i)
    return string(data), nil // graceful fallback
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: check remaining bytes before the call
if start < 0 || start+length > len(data) {
    return string(data), nil // use raw, skip escape decoding
}

Type guard

func hasCompleteHexEscape(data []byte, start, length int) bool {
    return start >= 0 && start+length <= len(data)
}

Try / catch

value, n, err := decodeHexEscape(data, i+2, 2)
if err != nil {
    log.Printf("bad escape at %d: %v", i, err)
    return string(data), nil // fallback to raw string
}

Prevention

When it happens

Trigger: Parsing a JS string where a \x escape (or \u escape feeding decodeHexEscape) appears with fewer than `length` hex-capable bytes left: data ends immediately after \x, or after \x with only one hex digit.

Common situations: Scraping haitunsou pages whose embedded JavaScript was truncated by the upstream site, cut off by a proxy/CDN, or whose encoder emitted a malformed \x escape at the very end of a string literal.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/79ab64f3ed47ea7f. Report an issue: GitHub.

Appendix: source

Thrown at plugin/haitunsou/haitunsou.go:254

			if r >= 0xD800 && r <= 0xDBFF && next+6 <= len(encoded) && encoded[next] == '\\' && encoded[next+1] == 'u' {
				low, lowNext, lowErr := decodeHexEscape(encoded, next+2, 4)
				if lowErr == nil && low >= 0xDC00 && low <= 0xDFFF {
					r = utf16.DecodeRune(r, rune(low))
					index = lowNext - 1
				}
			}
			decoded = utf8.AppendRune(decoded, r)
		default:
			decoded = append(decoded, encoded[index])
		}
	}
	return decoded, nil
}

func decodeHexEscape(data []byte, start, length int) (uint64, int, error) {
	end := start + length
	if start < 0 || end > len(data) {
		return 0, start, fmt.Errorf("转义序列不完整")
	}
	value, err := strconv.ParseUint(string(data[start:end]), 16, 16)
	if err != nil {
		return 0, start, fmt.Errorf("无效十六进制转义: %w", err)
	}
	return value, end, nil
}

func convertItem(item searchItem) (model.SearchResult, bool) {
	rawTitle := cleanText(item.Title)
	if rawTitle == "" {
		rawTitle = cleanText(item.Name)
	}
	if rawTitle == "" {
		return model.SearchResult{}, false
	}
	title, description := splitTitleAndDescription(rawTitle)
	if title == "" {

View on GitHub (pinned to beaa561337)