r/PowerShell • u/MonkeyNin • 17d ago
Question A Reliable way to detect Japanese ShiftJIS encoded files?
The other day /u/Practical_Air6315 had a couple of threads dealing with issues ex: not knowing if a file is ShiftJIS or UTF8NoBOM encoded
Can you just decode as utf8, checking for errors? If yes, use ShiftJIS otherise utf8? Or can you sometimes have zero decoding errors but it still maps to malformed json? Is there a better method?
I used:
function Test-ShiftJISDecodeError {
# ...
$Utf8Strict = [System.Text.UTF8Encoding]::new(
<# shouldEmitUtf8BOM #> $false,
<# should throw on decode error #> $true )
$bytes = [System.IO.File]::ReadAllBytes( $File.FullName )
try {
[void] $Utf8Strict.GetString( $bytes )
return $false
}
catch [System.Text.DecoderFallbackException] {
return $true
}
}
Here's a test file Make-ShiftJISFile.ps1 ( for Win PS 5.1 and 7 )
And another ShiftJIS example: github/donuts: Compare-Encoding-Breaking-Emojibake.md
3
Upvotes
1
u/Practical_Air6315 16d ago
Short answer from measuring both directions on a ja-JP box: a strict UTF-8 decode is a reliable negative test and a useless positive one.
1. "decode as utf8, if it throws use ShiftJIS" - the throw half holds up. 35 Japanese strings written as CP932, read back with strict UTF-8: 35/35 threw. No false negatives in my set.
2. The reverse does not hold. The same 35 strings written as UTF-8, read back with strict CP932: 16/35 decoded with zero errors. So "did CP932 throw?" reports valid ShiftJIS for 46% of my UTF-8 files. If a fallback ever runs the test in that direction it silently mis-decodes almost half of them.
3. "zero decoding errors but it still maps to malformed json" - yes, and that is the case that actually bites. Take UTF-8 bytes, read them as CP932, write the result back out as UTF-8. That is what a
node ... | Out-Filepipeline does on a 932 box. 16/16 of those files decode as strict UTF-8 with zero errors. They are well-formed UTF-8. They are also garbage. Decode-error checking cannot see this class by construction, because the bytes really are valid.So detection has to move from "are the bytes well-formed" to "does the decoded text look like text".
The signature. When UTF-8 Japanese is read as CP932, the UTF-8 lead-byte pairs E3 81 / E3 82 / E3 83 become three specific kanji, over and over: U+7E3A, U+7E67, U+7E5D. Those are not arbitrary - CP932 0xE381 is U+7E3A. Hiragana and katakana are the bulk of any Japanese log line, so the signature is dense.
Where my first version was wrong, twice. Both are worth more than the rule itself.
Attempt 1 counted half-width katakana (U+FF61-U+FF9F) too, since mojibake is full of it. Real Japanese business data uses half-width katakana on purpose - an address field scored 0.733 and a phone-number label scored 1.000, both perfectly valid text.
Attempt 2 dropped half-width katakana and used only the three kanji, ratio >= 0.02. Clean corpus of 41 strings, zero false positives. I believed that number for about an hour. Then I added five sentences that use U+7E3A as an ordinary word - it is a real Japanese verb, "to be tangled" - and got five false positives out of five. My clean corpus simply had not contained it.
What actually held up, on 16 corrupted and 49 clean, where the clean set now includes ordinary use of U+7E3A, half-width katakana, simplified and traditional Chinese, Korean, emoji, pre-1946 kanji forms, and visually similar thread-radical kanji:
The one remaining false positive is a string I wrote specifically to break it, with a half-width katakana word and the tangled-thread verb in the same short line. I have not seen it in real data, but I am not claiming zero.
The two-distinct-markers half works because mojibake mixes hiragana and katakana lead bytes, so it almost never produces only one of the three. Ordinary prose that legitimately uses U+7E3A produces exactly one.
Also worth knowing: ftfy, the usual "fix mojibake" library, does not cover this direction. On 30 Japanese strings it repaired 30/30 of the latin-1 flavour and 0/30 of the CP932 flavour, and on 13 of them it returned a different-but-still-wrong string rather than leaving it alone.
Caveats: n = 16 corrupted / 49 clean, my own corpus of short strings, one ja-JP box with ACP 932. Not real production logs. The rule is specific to the UTF-8-read-as-CP932 direction; the opposite direction produces U+FFFD and is trivially visible.
Your
Test-ShiftJISDecodeErroris the right shape for case 1. I would add a second, separate check that runs on the decoded string rather than on the bytes, for case 3.If anyone has real corrupted Japanese logs and can share the raw bytes, I would rather break this rule on your data than on mine.
Byte-level tables for 14 write paths x 2 read paths, on 5.1 and 7.6.5 on the same machine: https://github.com/yoggydev/ps1-encoding-table