Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/chardet/pipeline/utf1632.py: 15%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

129 statements  

1"""Stage 1a+: UTF-16/UTF-32 detection for data without BOM. 

2 

3This stage runs after BOM detection but before binary detection. 

4UTF-16 and UTF-32 encoded text contains characteristic null-byte patterns 

5that would otherwise cause binary detection to reject the data. 

6 

7Note: ``from __future__ import annotations`` is intentionally omitted because 

8this module is compiled with mypyc, which does not support PEP 563 string 

9annotations. 

10""" 

11 

12import codecs 

13import unicodedata 

14 

15from chardet.pipeline import ASCII_TEXT_BYTES, DETERMINISTIC_CONFIDENCE, DetectionResult 

16 

17# How many bytes to sample for pattern analysis 

18_SAMPLE_SIZE = 4096 

19 

20# Minimum bytes needed for reliable pattern detection 

21_MIN_BYTES_UTF32 = 16 # 4 full code units 

22_MIN_BYTES_UTF16 = 10 # 5 full code units 

23 

24# Minimum fraction of null bytes in the expected position for UTF-16. 

25# CJK-heavy UTF-16 text (Chinese, Japanese, Korean) can have as few as 

26# ~4.5% null bytes in the expected position, since CJK codepoints have 

27# non-zero high bytes. The validation step (decode + text quality check) 

28# prevents false positives from binary files at this lower threshold. 

29_UTF16_MIN_NULL_FRACTION = 0.03 

30 

31# Minimum text-quality score to accept a UTF-16 candidate when both 

32# endiannesses show null-byte patterns. A score of 0.5 corresponds to 

33# roughly 50% letters with no ASCII bonus (or ~40% with whitespace 

34# present) — sufficient to distinguish real text from coincidental byte 

35# patterns. 

36_MIN_TEXT_QUALITY = 0.5 

37 

38# Quality margin a byte order with the weaker null signal must win by to 

39# override the null pattern's choice. Keeps noisy near-ties (e.g. two 

40# alphabetic decodings of similar letter density) on the side the null 

41# evidence favors, while letting a clear gap (real CJK vs its byte-swapped 

42# scatter across the BMP) flip the answer. 

43_QUALITY_TIE_MARGIN = 0.05 

44 

45# Minimum fraction of printable characters for a decoded sample to be 

46# considered text rather than binary data. 

47_MIN_PRINTABLE_FRACTION = 0.7 

48 

49# Maximum null fraction (in the candidate null-byte position) below which 

50# the data is checked for a null-separator pattern. If the null fraction 

51# is below this AND all non-null bytes are printable ASCII, the candidate 

52# is rejected as a null-separator false positive rather than real UTF-16. 

53# 

54# When every non-null byte is printable ASCII, the two readings are easy 

55# to tell apart by how many code units carry a null. UTF-16 text made of 

56# ASCII characters puts a null in every unit, and the only thing that 

57# lowers the fraction is a stretch that fell out of phase (the corpus has 

58# a UTF-16-BE file with one such stretch, at 0.865). Null-separated 

59# records (``find -print0``, ``git ls-tree -z``) put a null in at most 

60# half the units once records are two characters or longer: three-byte 

61# ``abc\0`` records on one parity give exactly 0.5, and longer records 

62# give less. Single-character records are byte-identical to UTF-16 and 

63# are read as UTF-16. 0.75 sits between the two shapes. 

64_NULL_SEPARATOR_MAX_FRACTION = 0.75 

65 

66# ASCII_TEXT_BYTES plus the null byte — used by the null-separator guard 

67# to check whether non-null bytes are all printable ASCII. 

68_NULL_SEPARATOR_ALLOWED: bytes = b"\x00" + ASCII_TEXT_BYTES 

69 

70 

71def _is_null_separator_pattern(data: bytes, null_frac: float) -> bool: 

72 """Return True if the data looks like ASCII with null byte separators. 

73 

74 :param data: The raw byte sample to examine. 

75 :param null_frac: The positional null fraction for this UTF-16 candidate 

76 (i.e. fraction of null bytes in even positions for BE, or odd positions 

77 for LE) — not the total null fraction across all bytes. 

78 

79 Checks two conditions: 

80 1. The positional null fraction is below ``_NULL_SEPARATOR_MAX_FRACTION`` 

81 2. Every non-null byte is printable ASCII or common whitespace 

82 

83 When both conditions are met, the nulls are likely field separators 

84 (e.g. ``find -print0``), not UTF-16 encoding artifacts. 

85 """ 

86 if null_frac >= _NULL_SEPARATOR_MAX_FRACTION: 

87 return False 

88 return not data.translate(None, _NULL_SEPARATOR_ALLOWED) 

89 

90 

91def detect_utf1632_patterns(data: bytes) -> DetectionResult | None: 

92 """Detect UTF-32 or UTF-16 encoding from null-byte patterns. 

93 

94 UTF-32 is checked before UTF-16 since UTF-32 patterns are more specific. 

95 

96 :param data: The raw byte data to examine. 

97 :returns: A :class:`DetectionResult` if a strong pattern is found, or ``None``. 

98 """ 

99 sample = data[:_SAMPLE_SIZE] 

100 

101 if len(sample) < _MIN_BYTES_UTF16: 

102 return None 

103 

104 # Check UTF-32 first (more specific pattern) 

105 result = _check_utf32(sample) 

106 if result is not None: 

107 return result 

108 

109 # Then check UTF-16 

110 return _check_utf16(sample) 

111 

112 

113def _check_utf32(data: bytes) -> DetectionResult | None: 

114 """Check for UTF-32 encoding based on 4-byte unit structure. 

115 

116 For valid Unicode (U+0000 to U+10FFFF = 0x0010FFFF): 

117 - UTF-32-BE: the first byte of each 4-byte unit is always 0x00 

118 - UTF-32-LE: the last byte of each 4-byte unit is always 0x00 

119 

120 For BMP characters (U+0000 to U+FFFF), additionally: 

121 - UTF-32-BE: the second byte is also 0x00 

122 - UTF-32-LE: the third byte is also 0x00 

123 """ 

124 # Trim to a multiple of 4 bytes (like _check_utf16 trims to even length) 

125 trimmed_len = len(data) - (len(data) % 4) 

126 if trimmed_len < _MIN_BYTES_UTF32: 

127 return None 

128 data = data[:trimmed_len] 

129 

130 num_units = trimmed_len // 4 

131 

132 # UTF-32-BE: first byte of each 4-byte unit must be 0x00 

133 be_first_null = sum(1 for i in range(0, len(data), 4) if data[i] == 0) 

134 # Second byte is 0x00 for BMP characters (the vast majority of text) 

135 be_second_null = sum(1 for i in range(0, len(data), 4) if data[i + 1] == 0) 

136 

137 if be_first_null == num_units and be_second_null / num_units > 0.5: 

138 try: 

139 text = data.decode("utf-32-be") 

140 if _looks_like_text(text): 

141 return DetectionResult( 

142 encoding="utf-32-be", 

143 confidence=DETERMINISTIC_CONFIDENCE, 

144 language=None, 

145 ) 

146 except UnicodeDecodeError: 

147 pass 

148 

149 # UTF-32-LE: last byte of each 4-byte unit must be 0x00 

150 le_last_null = sum(1 for i in range(3, len(data), 4) if data[i] == 0) 

151 # Third byte is 0x00 for BMP characters 

152 le_third_null = sum(1 for i in range(2, len(data), 4) if data[i] == 0) 

153 

154 if le_last_null == num_units and le_third_null / num_units > 0.5: 

155 try: 

156 text = data.decode("utf-32-le") 

157 if _looks_like_text(text): 

158 return DetectionResult( 

159 encoding="utf-32-le", 

160 confidence=DETERMINISTIC_CONFIDENCE, 

161 language=None, 

162 ) 

163 except UnicodeDecodeError: 

164 pass 

165 

166 return None 

167 

168 

169def _check_utf16(data: bytes) -> DetectionResult | None: 

170 """Check for UTF-16 via null-byte patterns in alternating positions. 

171 

172 UTF-16 encodes each BMP character as two bytes. For characters whose 

173 code-point high byte is 0x00 (Latin, digits, basic punctuation, many 

174 control structures), one of the two bytes in each unit will be a null. 

175 Even for non-Latin scripts (Arabic, CJK, Cyrillic, etc.) a significant 

176 fraction of code units still contain at least one null byte. 

177 

178 Non-UTF-16 single-byte encodings never contain null bytes, so even a 

179 small null-byte fraction in alternating positions is a strong signal. 

180 

181 The null pattern only establishes that the data is UTF-16-like. Byte 

182 order is always chosen by decoding both ways and comparing text-quality 

183 scores, with the null signal breaking near-ties (see below). 

184 """ 

185 sample_len = min(len(data), _SAMPLE_SIZE) 

186 sample_len -= sample_len % 2 

187 if sample_len < _MIN_BYTES_UTF16: # pragma: no cover - caller checks length 

188 return None 

189 

190 num_units = sample_len // 2 

191 

192 # Count null bytes in even positions (UTF-16-BE high byte for ASCII) 

193 be_null_count = sum(1 for i in range(0, sample_len, 2) if data[i] == 0) 

194 # Count null bytes in odd positions (UTF-16-LE high byte for ASCII) 

195 le_null_count = sum(1 for i in range(1, sample_len, 2) if data[i] == 0) 

196 

197 be_frac = be_null_count / num_units 

198 le_frac = le_null_count / num_units 

199 

200 le_qualified = ( 

201 le_frac >= _UTF16_MIN_NULL_FRACTION 

202 and not _is_null_separator_pattern(data[:sample_len], le_frac) 

203 ) 

204 be_qualified = ( 

205 be_frac >= _UTF16_MIN_NULL_FRACTION 

206 and not _is_null_separator_pattern(data[:sample_len], be_frac) 

207 ) 

208 

209 if not (le_qualified or be_qualified): 

210 return None 

211 

212 # The null-byte pattern only establishes that the data is UTF-16-like; 

213 # it cannot be trusted to pick the byte order on its own. In pure-CJK 

214 # text with no ASCII at all, the only null bytes come from the *low* 

215 # byte of characters like U+4E00, which sit in the opposite parity 

216 # position and vote for the swapped byte order. Decode both ways and 

217 # let text quality decide. Sides are visited in null-signal order and 

218 # a challenger must win by a clear margin, so noisy near-ties keep the 

219 # answer the null pattern chose. 

220 sides = [("utf-16-le", le_frac, le_qualified), ("utf-16-be", be_frac, be_qualified)] 

221 if be_frac > le_frac: 

222 sides.reverse() 

223 

224 best_encoding: str | None = None 

225 best_quality = -2.0 

226 best_qualified = False 

227 viable = 0 

228 qualified_side_decoded = False 

229 

230 for encoding, _frac, qualified in sides: 

231 # final=False: a surrogate pair cut at the sample's end is not an error. 

232 decoder = codecs.getincrementaldecoder(encoding)() 

233 try: 

234 text = decoder.decode(data[:sample_len], final=False) 

235 except UnicodeDecodeError: 

236 continue 

237 if qualified: 

238 qualified_side_decoded = True 

239 if not _looks_like_text(text): 

240 continue 

241 viable += 1 

242 quality = _text_quality(text) 

243 if quality > best_quality + (_QUALITY_TIE_MARGIN if viable > 1 else 0.0): 

244 best_quality = quality 

245 best_encoding = encoding 

246 best_qualified = qualified 

247 

248 if best_encoding is None: 

249 return None 

250 

251 if best_qualified: 

252 # The null pattern and the quality comparison agree. A sole viable 

253 # side keeps the old single-candidate behavior (no quality floor); 

254 # a contested choice must look like real text on its own merits. 

255 accepted = viable == 1 or best_quality >= _MIN_TEXT_QUALITY 

256 else: 

257 # A side that failed the null check can win only when a 

258 # null-qualified side actually decoded (so the quality comparison 

259 # was a fair fight, e.g. real CJK beating its byte-swapped scatter) 

260 # and the winner clears the quality floor. If the null-favored 

261 # side could not even decode, the data is corrupt in the only byte 

262 # order the evidence supports: report nothing rather than the swap. 

263 accepted = qualified_side_decoded and best_quality >= _MIN_TEXT_QUALITY 

264 if accepted: 

265 return DetectionResult( 

266 encoding=best_encoding, 

267 confidence=DETERMINISTIC_CONFIDENCE, 

268 language=None, 

269 ) 

270 

271 return None 

272 

273 

274def _looks_like_text(text: str) -> bool: 

275 """Quick check: is decoded text mostly printable characters.""" 

276 if not text: 

277 return False 

278 sample = text[:500] 

279 printable = sum(1 for c in sample if c.isprintable() or c in "\n\r\t") 

280 return printable / len(sample) > _MIN_PRINTABLE_FRACTION 

281 

282 

283def _text_quality(text: str, limit: int = 500) -> float: 

284 """Score how much *text* looks like real human-readable content. 

285 

286 Returns a score in the range [-1.0, ~1.6). Higher values indicate 

287 more natural text. The practical maximum is 1.5 for all-ASCII-letter 

288 input (1.6 approaches as sample size grows with all ASCII letters plus 

289 whitespace). A score of -1.0 means the content is almost certainly not 

290 valid text (too many control characters or combining marks). 

291 

292 Scoring factors: 

293 

294 * Base score: ratio of Unicode letters (category ``L*``) to sample length. 

295 * ASCII bonus: additional 0.5x weight for ASCII letters. This is the 

296 primary signal for disambiguating endianness — correct decoding of 

297 Latin-heavy text produces ASCII letters, wrong decoding produces CJK. 

298 * Space bonus: +0.1 when the sample contains at least one whitespace 

299 character and is longer than 20 characters. 

300 * Rejection: returns -1.0 if >10% control characters or >20% combining 

301 marks (category ``M*``). 

302 """ 

303 sample = text[:limit] 

304 n = len(sample) 

305 if n == 0: # pragma: no cover - callers always pass non-empty text 

306 return -1.0 

307 

308 letters = 0 

309 marks = 0 

310 spaces = 0 

311 controls = 0 

312 ascii_letters = 0 

313 

314 for c in sample: 

315 cat = unicodedata.category(c) 

316 if cat[0] == "L": 

317 letters += 1 

318 if ord(c) < 128: 

319 ascii_letters += 1 

320 elif cat[0] == "M": 

321 marks += 1 

322 elif cat == "Zs" or c in "\n\r\t": 

323 spaces += 1 

324 elif cat[0] == "C": 

325 controls += 1 

326 

327 # Reject data with many control characters or combining marks 

328 if controls / n > 0.1: 

329 return -1.0 

330 if marks / n > 0.2: 

331 return -1.0 

332 

333 score = letters / n 

334 # ASCII letters strongly indicate correct endianness 

335 score += (ascii_letters / n) * 0.5 

336 # Real text usually contains some whitespace 

337 if n > 20 and spaces > 0: 

338 score += 0.1 

339 

340 return score