diff --git a/scripts/generate_site.py b/scripts/generate_site.py index ee0115e46..a627dda60 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -4517,8 +4517,9 @@ def generate_files_page() -> str: "", '
', ' ', + # No pattern: the line md5sum or certutil prints is taken whole, and + # the page reads the hash out of it. ' ', '', "
", diff --git a/tests/test_site_r4_components.py b/tests/test_site_r4_components.py index ea5b6b5e4..03df811e6 100644 --- a/tests/test_site_r4_components.py +++ b/tests/test_site_r4_components.py @@ -265,5 +265,38 @@ class CopyButtonInTheHeader(unittest.TestCase): self.assertIn(".expressive-code .frame.has-title .copy { inset-block-start:", source) +class AChecksumLineIsAHash(unittest.TestCase): + """The lookup and the search took a bare hash only: the line md5sum, + shasum --tag, certutil or Get-FileHash prints was refused whole.""" + + def test_the_hash_is_read_out_of_the_line(self): + source = _read(SITE_TS) + code = (source[source.index("const HASH_LENGTHS"):source.index("\n", source.index("const HASH_LENGTHS"))] + + source[source.index("const normalizeHash"):source.index("// Tables")]) + md5, sha1 = "490f666e1afb15b7362b406ed1cea246", "b05def971d8ec59f346f2d9ac21fb742e3eb6917" + spaced = " ".join(sha1[i:i + 2] for i in range(0, 40, 2)) + cases = [ + f"{md5} scph5501.bin", + f"SHA1 (scph5501.bin) = {sha1}", + f"SHA1 hash of scph5501.bin:\r\n{spaced}\r\nCertUtil: -hashfile command completed successfully.", + f"Algorithm Hash Path\n--------- ---- ----\nSHA1 {sha1.upper()} C:\\bios\\scph5501.bin", + f" 0x{md5.upper()} ", + "8d8cb7e4", + "deadbeef.bin", + f"{md5} {sha1}", + ] + out = _node(code + f"console.log(JSON.stringify({json.dumps(cases)}.map((c) => " + "[normalizeHash(c), isHash(normalizeHash(c))])));") + self.assertEqual(json.loads(out), [ + [md5, True], [sha1, True], [sha1, True], [sha1, True], [md5, True], + ["8d8cb7e4", True], ["deadbeef.bin", False], [md5 + sha1, False], + ]) + + def test_the_form_takes_the_line(self): + source = _read(REPO_ROOT / "scripts" / "generate_site.py") + start = source.index('", start)]) + + if __name__ == "__main__": unittest.main() diff --git a/website/src/scripts/site.ts b/website/src/scripts/site.ts index aa1252de9..469aebee2 100644 --- a/website/src/scripts/site.ts +++ b/website/src/scripts/site.ts @@ -24,7 +24,21 @@ type Item = { }; const siteRoot = () => new URL(import.meta.env.BASE_URL.replace(/\/?$/, '/'), location.origin); -const normalizeHash = (raw: string) => raw.trim().toLowerCase().replace(/^0x/, '').replace(/\s+/g, ''); +// A pasted hash, or the line a checksum tool prints around it: md5sum +// (` `), BSD and shasum --tag (`SHA1 () = `), +// certutil (the bytes spaced on a line of their own), Get-FileHash's table. +// The one word of an MD5, SHA1 or SHA256's length is taken: an 8-character +// word in a file name searched for is no CRC32. None or two, and the text is +// kept as typed, which isHash refuses. +const normalizeHash = (raw: string) => { + const text = raw.trim().toLowerCase(); + const bare = text.replace(/^0x/, '').replace(/\s+/g, ''); + if (/^[0-9a-f]*$/.test(bare)) return bare; + const words = text.split('\n').flatMap((line) => + /^[0-9a-f ]+$/.test(line.trim()) ? [line.replace(/\s+/g, '')] : (line.match(/\b[0-9a-f]+\b/g) ?? [])); + const found = [...new Set(words.filter((word) => word.length > 8 && word.length in HASH_LENGTHS))]; + return found.length === 1 ? found[0] : bare; +}; const isHash = (value: string) => /^[0-9a-f]+$/.test(value) && value.length in HASH_LENGTHS; // Tables