增加 CRC Hex 输入并修复依赖漏洞

This commit is contained in:
mid
2026-08-19 15:54:22 +08:00
parent 0ae6db36cb
commit 8bc6032f38
13 changed files with 268 additions and 37 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ This file is mandatory for AI and coding agents working in this repository. Read
- UTF-8 / Hex has the permanent tool ID `utf8-hex` and route `/tools/utf8-hex`. It uses strict UTF-8 conversion. Hex input accepts continuous text or chunks separated by whitespace, English commas, or Chinese commas, with an optional `0x` prefix on each chunk. Invalid UTF-8 must fail rather than silently replace bytes. - UTF-8 / Hex has the permanent tool ID `utf8-hex` and route `/tools/utf8-hex`. It uses strict UTF-8 conversion. Hex input accepts continuous text or chunks separated by whitespace, English commas, or Chinese commas, with an optional `0x` prefix on each chunk. Invalid UTF-8 must fail rather than silently replace bytes.
- Base64 supports standard and URL-safe input. Decoded output supports strict UTF-8, continuous Hex, and `0xNN, ...` byte arrays. Reject invalid characters, padding placement, lengths, and non-canonical trailing bits. - Base64 supports standard and URL-safe input. Decoded output supports strict UTF-8, continuous Hex, and `0xNN, ...` byte arrays. Reject invalid characters, padding placement, lengths, and non-canonical trailing bits.
- AES-CMAC accepts exactly 16, 24, or 32 key bytes for AES-128, AES-192, or AES-256, defaulting to 16 bytes. Never pad or truncate the key. Key and Hex-message input accepts continuous Hex or whitespace/comma/Chinese-comma-separated chunks with optional `0x` prefixes. The CMAC tag remains exactly 16 bytes for every key size. Preserve RFC 4493 AES-128 and NIST SP 800-38B AES-192/256 vectors. - AES-CMAC accepts exactly 16, 24, or 32 key bytes for AES-128, AES-192, or AES-256, defaulting to 16 bytes. Never pad or truncate the key. Key and Hex-message input accepts continuous Hex or whitespace/comma/Chinese-comma-separated chunks with optional `0x` prefixes. The CMAC tag remains exactly 16 bytes for every key size. Preserve RFC 4493 AES-128 and NIST SP 800-38B AES-192/256 vectors.
- CRC supports BIN and multi-block Intel HEX, CRC-8/16/32/64 presets, and custom `Width`, `Poly`, `Init`, `XorOut`, `RefIn`, and `RefOut`. The Chinese UI labels `RefIn`/`RefOut` as input/output bit reversal; these flags do not reverse byte order or the complete input. Combined data is ordered by address without filling gaps, and per-block results remain available. - CRC supports inline Hex bytes, BIN, and multi-block Intel HEX, with CRC-8/16/32/64 presets and custom `Width`, `Poly`, `Init`, `XorOut`, `RefIn`, and `RefOut`. Inline Hex uses the shared strict byte parser: accept continuous Hex or chunks separated by whitespace, English commas, or Chinese commas, with an optional `0x` prefix on each chunk; reject empty input. Limit it to 1 MiB after parsing and 6 MiB of source characters, and parse/calculate it in a dedicated Worker. The Chinese UI labels `RefIn`/`RefOut` as input/output bit reversal; these flags do not reverse byte order or the complete input. Combined file data is ordered by address without filling gaps, and per-block results remain available.
- Firmware Hash accepts Intel HEX and computes SHA-256 and SHA-512. Effective-data mode concatenates sorted blocks without gap filling. Block-header mode prepends exactly two binary uint32 values, `startAddress` and data-only `dataLength`, to every block. Both fields are four bytes; Big Endian is the default; total input is `Header1 || Data1 || Header2 || Data2 ...`. Never hash a textual Hex representation of the header. - Firmware Hash accepts Intel HEX and computes SHA-256 and SHA-512. Effective-data mode concatenates sorted blocks without gap filling. Block-header mode prepends exactly two binary uint32 values, `startAddress` and data-only `dataLength`, to every block. Both fields are four bytes; Big Endian is the default; total input is `Header1 || Data1 || Header2 || Data2 ...`. Never hash a textual Hex representation of the header.
- RSA allows 2048, 3072, and 4096-bit keys with public exponent 65537. Prefer Web Crypto; dynamically load the node-forge fallback only when needed, and seed it from `crypto.getRandomValues`. Export public SPKI PEM, private PKCS#8 PEM, and separately copyable N/E continuous Hex or byte arrays. - RSA allows 2048, 3072, and 4096-bit keys with public exponent 65537. Prefer Web Crypto; dynamically load the node-forge fallback only when needed, and seed it from `crypto.getRandomValues`. Export public SPKI PEM, private PKCS#8 PEM, and separately copyable N/E continuous Hex or byte arrays.
- RSA Signature Verification is verification-only. Default to RSA-PSS; support switching between PKCS#1 v1.5 and RSA-PSS with SHA-256/384/512, and do not add SHA-1. Accept one SPKI, PKCS#1 public-key, or X.509 certificate PEM block, or N/E Hex parameters. Treat E as an unsigned Hex integer: accept odd-length input by padding one zero nibble on the left, so `10001` and `010001` both mean decimal 65537; other Hex byte inputs remain even-length. X.509 support only extracts the RSA public key and displays metadata; never represent it as certificate-chain, issuer, usage, validity, or trust validation. Verification accepts 1024-8192-bit moduli and warns below 2048 bits; key generation remains restricted to 2048/3072/4096 bits. - RSA Signature Verification is verification-only. Default to RSA-PSS; support switching between PKCS#1 v1.5 and RSA-PSS with SHA-256/384/512, and do not add SHA-1. Accept one SPKI, PKCS#1 public-key, or X.509 certificate PEM block, or N/E Hex parameters. Treat E as an unsigned Hex integer: accept odd-length input by padding one zero nibble on the left, so `10001` and `010001` both mean decimal 65537; other Hex byte inputs remain even-length. X.509 support only extracts the RSA public key and displays metadata; never represent it as certificate-chain, issuer, usage, validity, or trust validation. Verification accepts 1024-8192-bit moduli and warns below 2048 bits; key generation remains restricted to 2048/3072/4096 bits.
+14
View File
@@ -2,6 +2,20 @@
本文档记录面向使用者和维护者的重要变更。版本号以 `package.json` 为准。 本文档记录面向使用者和维护者的重要变更。版本号以 `package.json` 为准。
## 未发布
### Added
- CRC 工具增加“Hex 输入 / 文件”模式切换;内联 Hex 支持连续字节、空白、中英文逗号和逐段可选 `0x` 前缀,并在独立 Web Worker 中完成受限解析与计算。
### Fixed
- 共享 Hex 字节解析器现在拒绝连续中英文逗号形成的空片段,避免输入格式错误被静默忽略。
### Security
- 将开发工具链中的传递依赖 `nanoid` 更新到 `3.3.18`,消除自定义生成器在长度为零时可能无限循环的已知漏洞。
## 0.3.0 - 2026-08-13 ## 0.3.0 - 2026-08-13
### Added ### Added
+2 -2
View File
@@ -11,7 +11,7 @@ The authoritative application version is the `version` field in `package.json`.
- UTF-8 and Hex conversion at the canonical route `/tools/utf8-hex` - UTF-8 and Hex conversion at the canonical route `/tools/utf8-hex`
- Base64 and Base64 URL encoding/decoding, with UTF-8, continuous Hex, and source-code byte-array output - Base64 and Base64 URL encoding/decoding, with UTF-8, continuous Hex, and source-code byte-array output
- AES-CMAC with selectable AES-128, AES-192, and AES-256 keys - AES-CMAC with selectable AES-128, AES-192, and AES-256 keys
- CRC-8/16/32/64 presets and custom parameters for BIN and multi-block Intel HEX files - CRC-8/16/32/64 presets and custom parameters for inline Hex bytes, BIN, and multi-block Intel HEX files
- Firmware SHA-256/SHA-512 over Intel HEX effective data, optionally with an address/length header for each block - Firmware SHA-256/SHA-512 over Intel HEX effective data, optionally with an address/length header for each block
- RSA 2048/3072/4096-bit key-pair generation with PEM, N/E Hex, and byte-array output - RSA 2048/3072/4096-bit key-pair generation with PEM, N/E Hex, and byte-array output
- RSA PKCS#1 v1.5 and RSA-PSS signature verification (RSA-PSS by default) with SHA-256/384/512, PEM/X.509/N-E public keys, and text, Hex, raw-file, Intel HEX effective-data, or Intel HEX block-header input - RSA PKCS#1 v1.5 and RSA-PSS signature verification (RSA-PSS by default) with SHA-256/384/512, PEM/X.509/N-E public keys, and text, Hex, raw-file, Intel HEX effective-data, or Intel HEX block-header input
@@ -39,7 +39,7 @@ See [README.zh-CN.md](README.zh-CN.md) for the exact algorithm contracts, input
## Resource limits ## Resource limits
The browser rejects oversized or structurally dense inputs before costly allocation. The current limits are 64 MiB for firmware/signature files and merged Intel HEX effective data, 1 MiB for parsed inline signature-verification data with a 6 MiB input-character guard, 1,024-8,192 bits for verification RSA moduli, 500,000 non-empty Intel HEX records, 1,024 Intel HEX blocks, 32 MiB for compressed PNG files, 16,384 pixels per PNG side and 32,000,000 decoded pixels, 1-65,536 bytes for secure random output (default 16), and 100,000 characters/10,000 tokens for syntax highlighting. Highlighting falls back to complete plain text; it never truncates a result. The browser rejects oversized or structurally dense inputs before costly allocation. The current limits are 64 MiB for firmware/signature files and merged Intel HEX effective data, 1 MiB each for parsed inline CRC Hex and inline signature-verification data with a 6 MiB input-character guard, 1,024-8,192 bits for verification RSA moduli, 500,000 non-empty Intel HEX records, 1,024 Intel HEX blocks, 32 MiB for compressed PNG files, 16,384 pixels per PNG side and 32,000,000 decoded pixels, 1-65,536 bytes for secure random output (default 16), and 100,000 characters/10,000 tokens for syntax highlighting. Highlighting falls back to complete plain text; it never truncates a result.
## Docker deployment ## Docker deployment
+4 -3
View File
@@ -11,7 +11,7 @@
- UTF-8 与 Hex 互转,固定路径为 `/tools/utf8-hex` - UTF-8 与 Hex 互转,固定路径为 `/tools/utf8-hex`
- Base64 与 Base64 URL 编码、解码,解码结果支持 UTF-8、连续 Hex 和代码字节数组 - Base64 与 Base64 URL 编码、解码,解码结果支持 UTF-8、连续 Hex 和代码字节数组
- AES-CMAC,支持 AES-128、AES-192 和 AES-256 密钥 - AES-CMAC,支持 AES-128、AES-192 和 AES-256 密钥
- CRC-8/16/32/64 预设与自定义参数,支持 BIN 和多 block Intel HEX - CRC-8/16/32/64 预设与自定义参数,支持内联 Hex 字节、BIN 和多 block Intel HEX
- 固件 SHA-256 / SHA-512,支持 Intel HEX 有效数据和带地址/长度的 Block 校验头 - 固件 SHA-256 / SHA-512,支持 Intel HEX 有效数据和带地址/长度的 Block 校验头
- RSA 2048/3072/4096 位密钥对,导出 PEM,以及可分别复制的 N/E Hex 或代码字节数组 - RSA 2048/3072/4096 位密钥对,导出 PEM,以及可分别复制的 N/E Hex 或代码字节数组
- RSA 签名验证,支持 PKCS#1 v1.5、RSA-PSS(默认)、SHA-256/384/512、PEM/X.509/N-E 公钥,以及文本、Hex、原始文件、Intel HEX 有效数据和 Block 校验头 - RSA 签名验证,支持 PKCS#1 v1.5、RSA-PSS(默认)、SHA-256/384/512、PEM/X.509/N-E 公钥,以及文本、Hex、原始文件、Intel HEX 有效数据和 Block 校验头
@@ -72,7 +72,7 @@ CHANGELOG.md 版本变更、修复和验证记录
- UTF-8 / Hex 工具的固定 ID 为 `utf8-hex`,路径为 `/tools/utf8-hex`。它按 UTF-8 字节转换文本;Hex 输入接受连续形式,或以空白、英文逗号、中文逗号分隔的片段,并允许每个片段使用可选的 `0x` 前缀。无效 UTF-8 字节必须报错,不能静默替换字符。 - UTF-8 / Hex 工具的固定 ID 为 `utf8-hex`,路径为 `/tools/utf8-hex`。它按 UTF-8 字节转换文本;Hex 输入接受连续形式,或以空白、英文逗号、中文逗号分隔的片段,并允许每个片段使用可选的 `0x` 前缀。无效 UTF-8 字节必须报错,不能静默替换字符。
- Base64 支持标准和 URL-safe 形式;解码结果可显示为严格 UTF-8、连续 Hex 或 `0x48, 0x9F, ...` 字节数组。无效字符、错误填充和非规范编码必须报错。 - Base64 支持标准和 URL-safe 形式;解码结果可显示为严格 UTF-8、连续 Hex 或 `0x48, 0x9F, ...` 字节数组。无效字符、错误填充和非规范编码必须报错。
- AES-CMAC 密钥长度可选 128、192 或 256 位,默认 128 位,并分别严格要求 16、24 或 32 字节;不自动补零或截断。密钥和 Hex 消息接受连续 Hex,或空格、换行、英文/中文逗号分隔,并允许片段使用可选的 `0x` 前缀。CMAC 标签始终为 128 位;AES-128 向量遵循 RFC 4493,AES-192/256 向量遵循 NIST SP 800-38B。 - AES-CMAC 密钥长度可选 128、192 或 256 位,默认 128 位,并分别严格要求 16、24 或 32 字节;不自动补零或截断。密钥和 Hex 消息接受连续 Hex,或空格、换行、英文/中文逗号分隔,并允许片段使用可选的 `0x` 前缀。CMAC 标签始终为 128 位;AES-128 向量遵循 RFC 4493,AES-192/256 向量遵循 NIST SP 800-38B。
- CRC 支持 BIN 和多 block Intel HEX,提供 CRC-8/16/32/64 预设,并允许自定义 `Width`、`Poly`、`Init`、`XorOut`、`RefIn` 和 `RefOut`。`RefIn`/`RefOut` 在页面中标为输入/输出位反转,不表示字节顺序或整个数据倒序。总 CRC 对按地址升序拼接的有效数据计算,不填充地址空洞,同时输出每个 block 的结果。 - CRC 支持内联 Hex 字节、BIN 和多 block Intel HEX,提供 CRC-8/16/32/64 预设,并允许自定义 `Width`、`Poly`、`Init`、`XorOut`、`RefIn` 和 `RefOut`。内联 Hex 接受连续形式,或以空白、英文逗号、中文逗号分隔的片段,并允许每个片段使用可选的 `0x` 前缀;空输入和无效字节格式会被拒绝,解析与计算均在独立 Web Worker 中完成。`RefIn`/`RefOut` 在页面中标为输入/输出位反转,不表示字节顺序或整个数据倒序。文件模式的总 CRC 对按地址升序拼接的有效数据计算,不填充地址空洞,同时输出每个 block 的结果。
- 固件 Hash 只接受 Intel HEX,输出 SHA-256 和 SHA-512。“有效数据”与“Block 校验头”模式的确切字节流见“Intel HEX 与 Block 校验头固定语义”;默认校验头字节序为 Big Endian。 - 固件 Hash 只接受 Intel HEX,输出 SHA-256 和 SHA-512。“有效数据”与“Block 校验头”模式的确切字节流见“Intel HEX 与 Block 校验头固定语义”;默认校验头字节序为 Big Endian。
- RSA 密钥对工具只生成 2048、3072、4096 位密钥,公钥指数固定为 65537。优先使用 Web Crypto,只有本地提供者不可用或失败时才动态加载 `node-forge` 回退,其随机种子仍只来自 `crypto.getRandomValues`;公钥为 SPKI PEM,私钥为 PKCS#8 PEM,N/E 可分别显示为连续 Hex 或代码字节数组。 - RSA 密钥对工具只生成 2048、3072、4096 位密钥,公钥指数固定为 65537。优先使用 Web Crypto,只有本地提供者不可用或失败时才动态加载 `node-forge` 回退,其随机种子仍只来自 `crypto.getRandomValues`;公钥为 SPKI PEM,私钥为 PKCS#8 PEM,N/E 可分别显示为连续 Hex 或代码字节数组。
- RSA 签名验证工具只执行验证,不接收私钥或生成签名。它默认选择 RSA-PSS,并支持切换到 RSA PKCS#1 v1.5;Hash 支持 SHA-256/384/512,不支持 SHA-1。PSS 的 Salt Length 必须显式给出,默认取 Hash 长度与当前模数编码上限中的较小值,并受当前模数与 Hash 的编码上限约束。公钥可使用单个 SPKI `PUBLIC KEY`、PKCS#1 `RSA PUBLIC KEY`、X.509 `CERTIFICATE` PEM 块或 N/E Hex 参数;E 按无符号 Hex 整数解析,奇数位输入会在最左侧补一个 `0`,因此 `10001`、`010001` 均表示十进制 65537,其他 Hex 字节输入仍要求偶数位。X.509 只用于提取 RSA 公钥和展示 Subject、序列号、有效期,不验证证书链、颁发者、用途或可信状态。验证接受 1024 至 8192 位 RSA 公钥,低于 2048 位时显示兼容性警告。 - RSA 签名验证工具只执行验证,不接收私钥或生成签名。它默认选择 RSA-PSS,并支持切换到 RSA PKCS#1 v1.5;Hash 支持 SHA-256/384/512,不支持 SHA-1。PSS 的 Salt Length 必须显式给出,默认取 Hash 长度与当前模数编码上限中的较小值,并受当前模数与 Hash 的编码上限约束。公钥可使用单个 SPKI `PUBLIC KEY`、PKCS#1 `RSA PUBLIC KEY`、X.509 `CERTIFICATE` PEM 块或 N/E Hex 参数;E 按无符号 Hex 整数解析,奇数位输入会在最左侧补一个 `0`,因此 `10001`、`010001` 均表示十进制 65537,其他 Hex 字节输入仍要求偶数位。X.509 只用于提取 RSA 公钥和展示 Subject、序列号、有效期,不验证证书链、颁发者、用途或可信状态。验证接受 1024 至 8192 位 RSA 公钥,低于 2048 位时显示兼容性警告。
@@ -98,6 +98,7 @@ CHANGELOG.md 版本变更、修复和验证记录
| 功能 | 当前限制 | 超限行为 | | 功能 | 当前限制 | 超限行为 |
| --- | --- | --- | | --- | --- | --- |
| CRC 固件文件 | 最大 64 MiB | 首次选择时拒绝;替换时保留当前文件和结果 | | CRC 固件文件 | 最大 64 MiB | 首次选择时拒绝;替换时保留当前文件和结果 |
| CRC 内联 Hex | 解析后最大 1 MiB;原始输入最多 6 MiB 字符 | 在 Worker 中解析与计算,超限或空输入直接拒绝 |
| Intel HEX 非空记录 | 最多 500,000 条 | Worker 停止解析并显示错误 | | Intel HEX 非空记录 | 最多 500,000 条 | Worker 停止解析并显示错误 |
| Intel HEX 数据块 | 最多 1,024 个 block | Worker 停止解析并显示错误 | | Intel HEX 数据块 | 最多 1,024 个 block | Worker 停止解析并显示错误 |
| Intel HEX 合并后的有效数据 | 最大 64 MiB | 合并分配前拒绝,Worker 停止解析并显示错误 | | Intel HEX 合并后的有效数据 | 最大 64 MiB | 合并分配前拒绝,Worker 停止解析并显示错误 |
@@ -319,7 +320,7 @@ npm audit --audit-level=low
`npm run check` 会依次运行全部 Vitest 测试、TypeScript/Vue 类型检查和生产构建。服务端测试使用随机本地端口验证 SPA 直达路由、静态资源 404 与缓存策略、导航 API、请求大小限制及跨源浏览器写入拦截。 `npm run check` 会依次运行全部 Vitest 测试、TypeScript/Vue 类型检查和生产构建。服务端测试使用随机本地端口验证 SPA 直达路由、静态资源 404 与缓存策略、导航 API、请求大小限制及跨源浏览器写入拦截。
自动化测试还覆盖标准算法向量、Base64 规范性、Intel HEX 记录与 block 边界、固件 Hash 与大小端 Block 头向量、RSA PKCS#1 v1.5/PSS 三种 Hash、PEM/X.509/N-E 公钥解析、签名编码与 PSS 盐长边界、原始文件、Intel HEX 有效数据与大小端 Block 校验头的验签字节语义、文件替换成功提交、失败保留旧状态、过期 Worker 响应隔离和 Worker 生命周期异常、PNG 头部与像素限制、导航排序纯函数、目录/图标/懒加载组件一致性,以及原子写入失败后的临时文件清理。 自动化测试还覆盖标准算法向量、Base64 规范性、CRC 内联 Hex 已知向量/多种分隔格式/资源上限、Intel HEX 记录与 block 边界、固件 Hash 与大小端 Block 头向量、RSA PKCS#1 v1.5/PSS 三种 Hash、PEM/X.509/N-E 公钥解析、签名编码与 PSS 盐长边界、原始文件、Intel HEX 有效数据与大小端 Block 校验头的验签字节语义、文件替换成功提交、失败保留旧状态、过期 Worker 响应隔离和 Worker 生命周期异常、PNG 头部与像素限制、导航排序纯函数、目录/图标/懒加载组件一致性,以及原子写入失败后的临时文件清理。
## 增加工具 ## 增加工具
+3 -3
View File
@@ -1167,9 +1167,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.17", "version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
+1 -1
View File
@@ -54,7 +54,7 @@
"id": "crc", "id": "crc",
"title": "CRC", "title": "CRC",
"category": "checksum", "category": "checksum",
"description": "解析 BIN 或 Intel HEX 数据并计算 CRC。", "description": "输入 Hex 字节或解析 BIN、Intel HEX 数据并计算 CRC。",
"icon": "BadgeCheck" "icon": "BadgeCheck"
}, },
{ {
+5 -2
View File
@@ -36,11 +36,13 @@ export function parseHex(input: string, options?: number | ParseHexOptions): Uin
let tokenHasDigits = false let tokenHasDigits = false
let tokenPrefixSeen = false let tokenPrefixSeen = false
let sawToken = false let sawToken = false
let commaNeedsValue = false
for (let index = 0; index < trimmed.length; index += 1) { for (let index = 0; index < trimmed.length; index += 1) {
const character = trimmed[index] const character = trimmed[index]
if (isHexSeparator(character)) { if (isHexSeparator(character)) {
if ((character === ',' || character === ',') && !sawToken) throw new Error('十六进制片段缺少数值') if ((character === ',' || character === ',') && (!sawToken || commaNeedsValue)) throw new Error('十六进制片段缺少数值')
if (tokenActive && !tokenHasDigits) throw new Error('十六进制片段缺少数值') if (tokenActive && !tokenHasDigits) throw new Error('十六进制片段缺少数值')
if (character === ',' || character === ',') commaNeedsValue = true
tokenActive = false tokenActive = false
tokenHasDigits = false tokenHasDigits = false
tokenPrefixSeen = false tokenPrefixSeen = false
@@ -56,9 +58,10 @@ export function parseHex(input: string, options?: number | ParseHexOptions): Uin
digits += 1 digits += 1
tokenHasDigits = true tokenHasDigits = true
sawToken = true sawToken = true
commaNeedsValue = false
} }
if (tokenActive && !tokenHasDigits) throw new Error('十六进制片段缺少数值') if (tokenActive && !tokenHasDigits) throw new Error('十六进制片段缺少数值')
if (/[,,]$/.test(trimmed)) throw new Error('十六进制片段缺少数值') if (commaNeedsValue) throw new Error('十六进制片段缺少数值')
if (digits % 2 !== 0) throw new Error('十六进制字符数量必须为偶数') if (digits % 2 !== 0) throw new Error('十六进制字符数量必须为偶数')
const byteLength = digits / 2 const byteLength = digits / 2
+49 -5
View File
@@ -1,4 +1,8 @@
import { calculateCrc, calculateCrcChunks, formatCrc, type CrcParameters } from '@/lib/crc' import { calculateCrc, calculateCrcChunks, formatCrc, type CrcParameters } from '@/lib/crc'
import { parseHex } from '@/lib/bytes'
export const MAX_INLINE_CRC_DATA_BYTES = 1024 * 1024
export const MAX_INLINE_CRC_INPUT_CHARS = 6 * 1024 * 1024
export interface CrcWorkerBlock { export interface CrcWorkerBlock {
startAddress: number startAddress: number
@@ -17,7 +21,14 @@ export interface CrcWorkerCalculateRequest {
parameters: CrcParameters parameters: CrcParameters
} }
export type CrcWorkerRequest = CrcWorkerParseRequest | CrcWorkerCalculateRequest export interface CrcWorkerCalculateHexRequest {
type: 'calculate-hex'
id: number
input: string
parameters: CrcParameters
}
export type CrcWorkerRequest = CrcWorkerParseRequest | CrcWorkerCalculateRequest | CrcWorkerCalculateHexRequest
export interface CrcBlockResult { export interface CrcBlockResult {
startAddress: number startAddress: number
@@ -26,25 +37,58 @@ export interface CrcBlockResult {
crc: string crc: string
} }
export type CrcWorkerResponse = { type CrcWorkerParsedResponse = {
type: 'parsed' type: 'parsed'
id: number id: number
format: 'Intel HEX' | 'BIN' format: 'Intel HEX' | 'BIN'
totalBytes: number totalBytes: number
blockCount: number blockCount: number
warnings: string[] warnings: string[]
} | { }
type CrcWorkerCalculatedResponse = {
type: 'calculated' type: 'calculated'
id: number id: number
combinedResult: string combinedResult: string
blockResults: CrcBlockResult[] blockResults: CrcBlockResult[]
} | { }
type CrcWorkerCalculatedHexResponse = {
type: 'calculated-hex'
id: number
combinedResult: string
totalBytes: number
}
type CrcWorkerErrorResponse<TOperation extends CrcWorkerRequest['type']> = {
type: 'error' type: 'error'
id: number id: number
operation: CrcWorkerRequest['type'] operation: TOperation
error: string error: string
} }
export type CrcFileWorkerResponse = CrcWorkerParsedResponse
| CrcWorkerCalculatedResponse
| CrcWorkerErrorResponse<'parse' | 'calculate'>
export type CrcHexWorkerResponse = CrcWorkerCalculatedHexResponse
| CrcWorkerErrorResponse<'calculate-hex'>
export type CrcWorkerResponse = CrcFileWorkerResponse | CrcHexWorkerResponse
export function calculateCrcHexInput(input: string, parameters: CrcParameters) {
const data = parseHex(input, {
maxBytes: MAX_INLINE_CRC_DATA_BYTES,
maxInputChars: MAX_INLINE_CRC_INPUT_CHARS,
limitLabel: 'Hex 数据',
})
if (data.length === 0) throw new Error('请输入 Hex 数据')
return {
combinedResult: formatCrc(calculateCrc(data, parameters), parameters.width),
totalBytes: data.length,
}
}
export function calculateCrcReport(blocks: CrcWorkerBlock[], parameters: CrcParameters) { export function calculateCrcReport(blocks: CrcWorkerBlock[], parameters: CrcParameters) {
const combinedValue = calculateCrcChunks(blocks.map((block) => block.data), parameters) const combinedValue = calculateCrcChunks(blocks.map((block) => block.data), parameters)
const combinedResult = formatCrc(combinedValue, parameters.width) const combinedResult = formatCrc(combinedValue, parameters.width)
+2
View File
@@ -226,6 +226,8 @@ button[aria-disabled='true'], input[aria-disabled='true'], select[aria-disabled=
.file-drop-summary.is-replacing .file-drop-feedback { color: var(--muted); } .file-drop-summary.is-replacing .file-drop-feedback { color: var(--muted); }
.file-drop-spinner { animation: file-drop-spin .8s linear infinite; } .file-drop-spinner { animation: file-drop-spin .8s linear infinite; }
@keyframes file-drop-spin { to { transform: rotate(360deg); } } @keyframes file-drop-spin { to { transform: rotate(360deg); } }
.crc-source-controls { margin-bottom: 12px; }
.crc-hex-editor { margin-bottom: 16px; }
.crc-config { display: grid; grid-template-columns: minmax(190px, 1.35fr) minmax(96px, .65fr) repeat(3, minmax(145px, 1fr)); gap: 10px; align-items: end; } .crc-config { display: grid; grid-template-columns: minmax(190px, 1.35fr) minmax(96px, .65fr) repeat(3, minmax(145px, 1fr)); gap: 10px; align-items: end; }
.parameter-field { min-width: 0; display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 600; } .parameter-field { min-width: 0; display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 600; }
.parameter-field .select-input, .parameter-field .text-input { width: 100%; font-size: 12px; font-weight: 400; } .parameter-field .select-input, .parameter-field .text-input { width: 100%; font-size: 12px; font-weight: 400; }
+142 -18
View File
@@ -1,14 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp } from '@lucide/vue' import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp, Trash2 } from '@lucide/vue'
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue' import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import FileDropField from '@/components/FileDropField.vue' import FileDropField from '@/components/FileDropField.vue'
import ModeTabs from '@/components/ModeTabs.vue'
import TextEditor from '@/components/TextEditor.vue'
import ToolPage from '@/components/ToolPage.vue' import ToolPage from '@/components/ToolPage.vue'
import { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker' import { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker'
import { copyText } from '@/lib/browser' import { copyText } from '@/lib/browser'
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc' import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
import type { CrcBlockResult, CrcWorkerCalculateRequest, CrcWorkerResponse } from '@/lib/crcWorker' import {
MAX_INLINE_CRC_INPUT_CHARS,
type CrcBlockResult,
type CrcFileWorkerResponse,
type CrcHexWorkerResponse,
type CrcWorkerCalculateHexRequest,
type CrcWorkerCalculateRequest,
} from '@/lib/crcWorker'
import { MAX_FIRMWARE_FILE_SIZE, type FirmwareFormat } from '@/lib/firmware' import { MAX_FIRMWARE_FILE_SIZE, type FirmwareFormat } from '@/lib/firmware'
type CrcInputMode = 'hex' | 'file'
interface FirmwareData { interface FirmwareData {
fileName: string fileName: string
fileSize: number fileSize: number
@@ -19,6 +30,13 @@ interface FirmwareData {
} }
const defaultPreset = CRC_PRESETS[0] const defaultPreset = CRC_PRESETS[0]
const inputMode = ref<CrcInputMode>('file')
const inputModeOptions = [
{ value: 'hex' as const, label: 'Hex 输入' },
{ value: 'file' as const, label: '文件' },
]
const hexInput = ref('')
const hexResultBytes = ref<number>()
const firmware = shallowRef<FirmwareData>() const firmware = shallowRef<FirmwareData>()
const selectedPreset = ref(defaultPreset.id) const selectedPreset = ref(defaultPreset.id)
const width = ref(defaultPreset.width) const width = ref(defaultPreset.width)
@@ -31,6 +49,7 @@ const combinedResult = ref('')
const blockResults = ref<CrcBlockResult[]>([]) const blockResults = ref<CrcBlockResult[]>([])
const error = ref('') const error = ref('')
const success = ref('') const success = ref('')
const inlineCalculating = ref(false)
const activeParameterName = computed(() => CRC_PRESETS.find((preset) => preset.id === selectedPreset.value)?.name ?? '自定义参数') const activeParameterName = computed(() => CRC_PRESETS.find((preset) => preset.id === selectedPreset.value)?.name ?? '自定义参数')
@@ -72,10 +91,23 @@ function getParameters(): CrcParameters {
} }
} }
let inlineWorker: Worker | undefined
let inlineRequestSequence = 0
let activeInlineRequest: number | undefined
function destroyInlineWorker() {
inlineWorker?.terminate()
inlineWorker = undefined
activeInlineRequest = undefined
inlineCalculating.value = false
}
function clearResults() { function clearResults() {
invalidateCalculation() invalidateCalculation()
destroyInlineWorker()
combinedResult.value = '' combinedResult.value = ''
blockResults.value = [] blockResults.value = []
hexResultBytes.value = undefined
success.value = '' success.value = ''
} }
@@ -91,7 +123,7 @@ const {
calculate: calculateInWorker, calculate: calculateInWorker,
invalidateCalculation, invalidateCalculation,
destroyWorkers, destroyWorkers,
} = useTransactionalFileWorker<Omit<CrcWorkerCalculateRequest, 'type' | 'id'>, CrcWorkerResponse>({ } = useTransactionalFileWorker<Omit<CrcWorkerCalculateRequest, 'type' | 'id'>, CrcFileWorkerResponse>({
createWorker: () => new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' }), createWorker: () => new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' }),
onParsed: (response, selected) => { onParsed: (response, selected) => {
clearResults() clearResults()
@@ -125,6 +157,8 @@ const {
}) })
let activeCalculationName = '' let activeCalculationName = ''
const busy = computed(() => parsing.value || calculating.value || inlineCalculating.value)
const isCalculating = computed(() => calculating.value || inlineCalculating.value)
function selectFile(selected: File) { function selectFile(selected: File) {
if (selected.size === 0) { error.value = '文件为空'; return } if (selected.size === 0) { error.value = '文件为空'; return }
@@ -135,12 +169,71 @@ function selectFile(selected: File) {
parseFile(selected) parseFile(selected)
} }
function startHexCalculation(parameters: CrcParameters) {
let worker: Worker
try {
worker = new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' })
} catch {
error.value = 'CRC 后台线程创建失败'
return
}
const id = ++inlineRequestSequence
inlineWorker = worker
activeInlineRequest = id
inlineCalculating.value = true
worker.onmessage = (event: MessageEvent<CrcHexWorkerResponse>) => {
const response = event.data
if (worker !== inlineWorker || response.id !== activeInlineRequest) return
destroyInlineWorker()
if (response.type === 'error') {
error.value = response.error
return
}
combinedResult.value = response.combinedResult
hexResultBytes.value = response.totalBytes
success.value = `${activeCalculationName} 计算完成`
}
function handleWorkerFailure(event: Event) {
event.preventDefault()
if (worker !== inlineWorker || id !== activeInlineRequest) return
destroyInlineWorker()
error.value = 'CRC 后台线程异常终止,请重试'
}
worker.onerror = handleWorkerFailure
worker.onmessageerror = handleWorkerFailure
const request: CrcWorkerCalculateHexRequest = {
type: 'calculate-hex',
id,
input: hexInput.value,
parameters,
}
try {
worker.postMessage(request)
} catch {
destroyInlineWorker()
error.value = 'CRC 后台线程通信失败'
}
}
function calculate() { function calculate() {
invalidateResults() invalidateResults()
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
try { try {
const parameters = getParameters() const parameters = getParameters()
activeCalculationName = activeParameterName.value activeCalculationName = activeParameterName.value
if (inputMode.value === 'hex') {
if (!hexInput.value.trim()) throw new Error('请输入 Hex 数据')
if (hexInput.value.length > MAX_INLINE_CRC_INPUT_CHARS) {
throw new Error(`Hex 数据输入不能超过 ${MAX_INLINE_CRC_INPUT_CHARS.toLocaleString()} 个字符`)
}
startHexCalculation(parameters)
return
}
if (!firmware.value) throw new Error('请先选择 BIN 或 Intel HEX 文件')
const startStatus = calculateInWorker({ parameters }) const startStatus = calculateInWorker({ parameters })
if (startStatus === 'unavailable') throw new Error('文件数据已失效,请重新选择文件') if (startStatus === 'unavailable') throw new Error('文件数据已失效,请重新选择文件')
} catch (reason) { } catch (reason) {
@@ -159,12 +252,42 @@ function clearFile() {
firmware.value = undefined firmware.value = undefined
} }
onBeforeUnmount(destroyWorkers) function clearHexInput() {
hexInput.value = ''
invalidateResults()
}
watch(inputMode, invalidateResults)
watch(hexInput, () => {
if (inputMode.value === 'hex') invalidateResults()
})
onBeforeUnmount(() => {
destroyInlineWorker()
destroyWorkers()
})
</script> </script>
<template> <template>
<ToolPage category="校验计算" title="CRC" description="解析 BIN 或 Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。"> <ToolPage category="校验计算" title="CRC" description="输入 Hex 字节或解析 BIN、Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。">
<div class="tool-controls crc-source-controls">
<ModeTabs v-model="inputMode" :options="inputModeOptions" label="CRC 输入模式" :disabled="busy" />
</div>
<TextEditor
v-if="inputMode === 'hex'"
class="crc-hex-editor"
id="crc-hex-input"
v-model="hexInput"
label="待校验数据"
format="Hex 字节"
:meta="`${hexInput.length.toLocaleString()} 字符`"
placeholder="例如:A5 1F 7A DD;支持连续 Hex、空白、逗号和可选 0x 前缀"
:disabled="busy"
:rows="7"
/>
<FileDropField <FileDropField
v-else
input-id="crc-file" input-id="crc-file"
replacement-input-id="crc-replacement-file" replacement-input-id="crc-replacement-file"
accept=".hex,.ihx,.bin,application/octet-stream" accept=".hex,.ihx,.bin,application/octet-stream"
@@ -183,39 +306,40 @@ onBeforeUnmount(destroyWorkers)
<div class="crc-config"> <div class="crc-config">
<label class="parameter-field preset-field"><span>参数预设(Preset)</span> <label class="parameter-field preset-field"><span>参数预设(Preset)</span>
<select id="crc-preset" name="crc-preset" v-model="selectedPreset" class="select-input" :disabled="calculating" @change="applyPreset"> <select id="crc-preset" name="crc-preset" v-model="selectedPreset" class="select-input" :disabled="busy" @change="applyPreset">
<option v-for="preset in CRC_PRESETS" :key="preset.id" :value="preset.id">{{ preset.name }}</option> <option v-for="preset in CRC_PRESETS" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
<option value="custom">自定义参数</option> <option value="custom">自定义参数</option>
</select> </select>
</label> </label>
<label class="parameter-field"><span>位宽(Width)</span> <label class="parameter-field"><span>位宽(Width)</span>
<select id="crc-width" name="crc-width" v-model.number="width" class="select-input" :disabled="calculating" @change="markCustom"><option :value="8">8</option><option :value="16">16</option><option :value="32">32</option><option :value="64">64</option></select> <select id="crc-width" name="crc-width" v-model.number="width" class="select-input" :disabled="busy" @change="markCustom"><option :value="8">8</option><option :value="16">16</option><option :value="32">32</option><option :value="64">64</option></select>
</label> </label>
<label class="parameter-field"><span>多项式(Poly,Hex)</span><input id="crc-polynomial" name="crc-polynomial" v-model="polynomial" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label> <label class="parameter-field"><span>多项式(Poly,Hex)</span><input id="crc-polynomial" name="crc-polynomial" v-model="polynomial" class="text-input" :disabled="busy" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>初始值(Init,Hex)</span><input id="crc-initial-value" name="crc-initial-value" v-model="initialValue" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label> <label class="parameter-field"><span>初始值(Init,Hex)</span><input id="crc-initial-value" name="crc-initial-value" v-model="initialValue" class="text-input" :disabled="busy" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>异或输出(XorOut,Hex)</span><input id="crc-xor-out" name="crc-xor-out" v-model="xorOut" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label> <label class="parameter-field"><span>异或输出(XorOut,Hex)</span><input id="crc-xor-out" name="crc-xor-out" v-model="xorOut" class="text-input" :disabled="busy" spellcheck="false" @input="markCustom" /></label>
<div class="crc-reflection-options" role="group" aria-label="位反转选项"> <div class="crc-reflection-options" role="group" aria-label="位反转选项">
<label class="check-field crc-check"><input id="crc-ref-in" name="crc-ref-in" v-model="inputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输入位反转(RefIn)</label> <label class="check-field crc-check"><input id="crc-ref-in" name="crc-ref-in" v-model="inputReflected" type="checkbox" :disabled="busy" @change="markCustom" />输入位反转(RefIn)</label>
<label class="check-field crc-check"><input id="crc-ref-out" name="crc-ref-out" v-model="outputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输出位反转(RefOut)</label> <label class="check-field crc-check"><input id="crc-ref-out" name="crc-ref-out" v-model="outputReflected" type="checkbox" :disabled="busy" @change="markCustom" />输出位反转(RefOut)</label>
</div> </div>
</div> </div>
<div class="action-row"> <div class="action-row">
<button class="action-button primary" type="button" :disabled="!firmware || parsing || calculating" @click="calculate"><Calculator :size="15" />{{ calculating ? '正在计算…' : '计算 CRC' }}</button> <button class="action-button primary" type="button" :disabled="busy || (inputMode === 'file' ? !firmware : !hexInput.trim())" @click="calculate"><Calculator :size="15" />{{ isCalculating ? '正在计算…' : '计算 CRC' }}</button>
<button v-if="inputMode === 'hex'" class="action-button" type="button" :disabled="busy || !hexInput" @click="clearHexInput"><Trash2 :size="15" />清空</button>
<span class="action-spacer" /> <span class="action-spacer" />
<button class="action-button ghost" type="button" :disabled="!combinedResult" @click="copyResult"><Copy :size="15" />复制总 CRC</button> <button class="action-button ghost" type="button" :disabled="!combinedResult" @click="copyResult"><Copy :size="15" />复制总 CRC</button>
</div> </div>
<div v-if="combinedResult" class="crc-results"> <div v-if="combinedResult" class="crc-results">
<div class="crc-total"><span>总有效数据 · 按地址升序拼接,不填充空洞</span><strong>{{ combinedResult }}</strong></div> <div class="crc-total"><span>{{ inputMode === 'hex' ? `Hex 输入 · ${hexResultBytes?.toLocaleString()} 字节` : '总有效数据 · 按地址升序拼接,不填充空洞' }}</span><strong>{{ combinedResult }}</strong></div>
<div class="result-table-wrap"> <div v-if="inputMode === 'file'" class="result-table-wrap">
<table class="result-table"> <table class="result-table">
<thead><tr><th>数据块(Block)</th><th>起始地址(Start Address)</th><th>结束地址(End Address)</th><th>字节数(Length)</th><th>CRC</th></tr></thead> <thead><tr><th>数据块(Block)</th><th>起始地址(Start Address)</th><th>结束地址(End Address)</th><th>字节数(Length)</th><th>CRC</th></tr></thead>
<tbody><tr v-for="(block, index) in blockResults" :key="block.startAddress"><td>#{{ index + 1 }}</td><td>{{ formatAddress(block.startAddress) }}</td><td>{{ formatAddress(block.endAddress) }}</td><td>{{ block.length.toLocaleString() }}</td><td class="crc-value">{{ block.crc }}</td></tr></tbody> <tbody><tr v-for="(block, index) in blockResults" :key="block.startAddress"><td>#{{ index + 1 }}</td><td>{{ formatAddress(block.startAddress) }}</td><td>{{ formatAddress(block.endAddress) }}</td><td>{{ block.length.toLocaleString() }}</td><td class="crc-value">{{ block.crc }}</td></tr></tbody>
</table> </table>
</div> </div>
</div> </div>
<div v-if="firmware?.warnings.length" class="status-message warning"><AlertCircle :size="16" />{{ firmware.warnings.join(';') }}</div> <div v-if="inputMode === 'file' && firmware?.warnings.length" class="status-message warning"><AlertCircle :size="16" />{{ firmware.warnings.join(';') }}</div>
<div v-if="error" class="status-message error"><AlertCircle :size="16" />{{ error }}</div> <div v-if="error" class="status-message error"><AlertCircle :size="16" />{{ error }}</div>
<div v-else-if="success" class="status-message success"><CheckCircle2 :size="16" />{{ success }}</div> <div v-else-if="success" class="status-message success"><CheckCircle2 :size="16" />{{ success }}</div>
</ToolPage> </ToolPage>
+9 -1
View File
@@ -1,4 +1,4 @@
import { calculateCrcReport, type CrcWorkerRequest, type CrcWorkerResponse } from '@/lib/crcWorker' import { calculateCrcHexInput, calculateCrcReport, type CrcWorkerRequest, type CrcWorkerResponse } from '@/lib/crcWorker'
import { getFirmwareFormat, MAX_FIRMWARE_FILE_SIZE, parseFirmwareContent } from '@/lib/firmware' import { getFirmwareFormat, MAX_FIRMWARE_FILE_SIZE, parseFirmwareContent } from '@/lib/firmware'
import type { DataBlock } from '@/lib/intelHex' import type { DataBlock } from '@/lib/intelHex'
@@ -31,6 +31,14 @@ workerScope.onmessage = async (event) => {
}) })
return return
} }
if (request.type === 'calculate-hex') {
workerScope.postMessage({
type: 'calculated-hex',
id: request.id,
...calculateCrcHexInput(request.input, request.parameters),
})
return
}
if (!firmwareBlocks) throw new Error('请先选择 BIN 或 Intel HEX 文件') if (!firmwareBlocks) throw new Error('请先选择 BIN 或 Intel HEX 文件')
workerScope.postMessage({ type: 'calculated', id: request.id, ...calculateCrcReport(firmwareBlocks, request.parameters) }) workerScope.postMessage({ type: 'calculated', id: request.id, ...calculateCrcReport(firmwareBlocks, request.parameters) })
} catch (reason) { } catch (reason) {
+2
View File
@@ -28,6 +28,8 @@ describe('parseHex', () => {
expect(() => parseHex('0011', 16)).toThrow('当前为 2 字节') expect(() => parseHex('0011', 16)).toThrow('当前为 2 字节')
expect(() => parseHex(',AA')).toThrow('片段缺少') expect(() => parseHex(',AA')).toThrow('片段缺少')
expect(() => parseHex('AA,')).toThrow('片段缺少') expect(() => parseHex('AA,')).toThrow('片段缺少')
expect(() => parseHex('AA,,BB')).toThrow('片段缺少')
expect(() => parseHex('AA, ,BB')).toThrow('片段缺少')
}) })
it('supports bounded parsing without allocating oversized output', () => { it('supports bounded parsing without allocating oversized output', () => {
+34 -1
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { calculateCrc, calculateCrcChunks, CRC_PRESETS, formatCrc } from '../src/lib/crc' import { calculateCrc, calculateCrcChunks, CRC_PRESETS, formatCrc } from '../src/lib/crc'
import { calculateCrcReport } from '../src/lib/crcWorker' import {
calculateCrcHexInput,
calculateCrcReport,
MAX_INLINE_CRC_DATA_BYTES,
MAX_INLINE_CRC_INPUT_CHARS,
} from '../src/lib/crcWorker'
import { getFirmwareFormat, parseFirmwareContent } from '../src/lib/firmware' import { getFirmwareFormat, parseFirmwareContent } from '../src/lib/firmware'
import { concatenateBlocks, parseIntelHex } from '../src/lib/intelHex' import { concatenateBlocks, parseIntelHex } from '../src/lib/intelHex'
import { textToBytes } from '../src/lib/bytes' import { textToBytes } from '../src/lib/bytes'
@@ -48,6 +53,34 @@ describe('CRC', () => {
expect(calculateCrcChunks(chunks, preset)).toBe(calculateCrc(textToBytes('123456789'), preset)) expect(calculateCrcChunks(chunks, preset)).toBe(calculateCrc(textToBytes('123456789'), preset))
}) })
it('calculates inline Hex with the same separators used by other tools', () => {
const preset = CRC_PRESETS[0]
const inputs = [
'A51F7ADD',
'A5 1F 7A DD',
'A5,1F,7A,DD',
'A5,1F,7A,DD',
'0xA5 0x1F 0x7A 0xDD',
'0xa5, 1F\n0x7a,DD',
]
for (const input of inputs) {
expect(calculateCrcHexInput(input, preset)).toEqual({
combinedResult: '6139ED73',
totalBytes: 4,
})
}
})
it('rejects empty, malformed, and oversized inline Hex', () => {
const preset = CRC_PRESETS[0]
expect(() => calculateCrcHexInput(' ', preset)).toThrow('请输入 Hex 数据')
expect(() => calculateCrcHexInput('ABC', preset)).toThrow('偶数')
expect(() => calculateCrcHexInput('A5,,1F', preset)).toThrow('片段缺少')
expect(() => calculateCrcHexInput('GG', preset)).toThrow('无效')
expect(() => calculateCrcHexInput('AA'.repeat(MAX_INLINE_CRC_DATA_BYTES + 1), preset)).toThrow('Hex 数据不能超过 1 MiB')
expect(() => calculateCrcHexInput(' '.repeat(MAX_INLINE_CRC_INPUT_CHARS + 1), preset)).toThrow('输入不能超过')
})
it('rejects malformed runtime parameters instead of coercing them', () => { it('rejects malformed runtime parameters instead of coercing them', () => {
const preset = CRC_PRESETS[0] const preset = CRC_PRESETS[0]
expect(() => calculateCrc(new Uint8Array(), { ...preset, inputReflected: undefined as never })).toThrow('布尔值') expect(() => calculateCrc(new Uint8Array(), { ...preset, inputReflected: undefined as never })).toThrow('布尔值')