Compare commits

..
1 Commits
Author SHA1 Message Date
mid 1aea179f19 扩充 CRC 预设并发布日期版本 2026.8.22 2026-08-22 16:36:37 +08:00
10 changed files with 153 additions and 61 deletions
+2 -2
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.
- 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.
- 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.
- CRC supports inline Hex bytes, BIN, and multi-block Intel HEX, with 16 reviewed CRC-8/16/32/64 presets and custom `Width`, `Poly`, `Init`, `XorOut`, `RefIn`, and `RefOut`. Keep canonical preset names and visible aliases synchronized with `src/lib/crc.ts`: CRC-8/SMBUS, MAXIM-DOW, SAE-J1850; CRC-16/ARC, DNP, IBM-3740, IBM-SDLC, KERMIT, MODBUS, USB, XMODEM; CRC-32/ISO-HDLC, ISCSI, MPEG-2; CRC-64/ECMA-182, XZ. Show the ambiguous `CCITT` alias only alongside KERMIT and `CCITT-FALSE` only alongside IBM-3740; never collapse them into one ambiguous preset. 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.
- 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.
@@ -109,7 +109,7 @@ For a release, synchronize these five tracked files:
4. `README.zh-CN.md`
5. `CHANGELOG.md`
Use `npm version patch --no-git-tag-version` or the appropriate minor/major form so the project version in `package.json` and the two project metadata entries in `package-lock.json` remain synchronized. Other lockfile `version` fields belong to dependencies. `README.md` intentionally does not hardcode the current version.
Use an npm/SemVer-compatible calendar version in `YYYY.M.D` form, without leading zeroes in the numeric fields; for example, `2026.8.22`. Allow at most one final release per calendar day and use `YYYY.M.D-rc.N` only for prerelease validation. Run `npm version YYYY.M.D --no-git-tag-version` so the project version in `package.json` and the two project metadata entries in `package-lock.json` remain synchronized. Other lockfile `version` fields belong to dependencies. `README.md` intentionally does not hardcode the current version.
Rebuild `dist`, the requested delivery ZIP, optional Docker image tar, and SHA-256 checksums. Generated assets are release outputs and remain ignored by Git. Never include `node_modules`, `.env`, runtime data, credentials, or local logs.
+7
View File
@@ -4,9 +4,16 @@
## 未发布
## 2026.8.22 - 2026-08-22
### Added
- CRC 工具增加“Hex 输入 / 文件”模式切换;内联 Hex 支持连续字节、空白、中英文逗号和逐段可选 `0x` 前缀,并在独立 Web Worker 中完成受限解析与计算。
- CRC 标准预设由 6 种扩充为 16 种,增加 MAXIM-DOW、SAE-J1850、DNP、IBM-SDLC、KERMIT、USB、ISCSI、MPEG-2、ECMA-182 和 XZ,并补齐可直接选择的 CRC-64 预设。
### Changed
- CRC 预设按位宽分组,并采用规范名称及常用别名;原 CCITT-FALSE 显示为 IBM-3740(CCITT-FALSE),原 CRC-8 显示为 SMBUS(CRC-8),同时明确区分 KERMIT(CCITT)。
### Fixed
+2 -1
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`
- 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
- CRC-8/16/32/64 presets and custom parameters for inline Hex bytes, BIN, and multi-block Intel HEX files
- 16 reviewed 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
- 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
@@ -30,6 +30,7 @@ The authoritative application version is the `version` field in `package.json`.
- The first-run default category order is Encoding, Checksum, Cryptography, Formatting, and Image. A saved `navigation.json` intentionally keeps its existing custom order; it is not silently reset by an application update.
- Theme selection is stored in same-origin browser `localStorage`. The only server-side application data is `navigation.json`.
- CRC and Firmware Hash share the same file picker and loaded-file summary. After a file is loaded, a replacement can be dropped directly onto that summary. The candidate is parsed in a separate Worker and replaces the active file only after successful validation; an invalid replacement preserves the current file and results.
- CRC presets are grouped by width and use canonical names. They cover CRC-8/SMBUS, MAXIM-DOW, and SAE-J1850; CRC-16/ARC, DNP, IBM-3740, IBM-SDLC, KERMIT, MODBUS, USB, and XMODEM; CRC-32/ISO-HDLC, ISCSI, and MPEG-2; and CRC-64/ECMA-182 and XZ. Familiar aliases such as CRC-8, CCITT-FALSE, CCITT, X-25, and CRC-32C remain visible to disambiguate otherwise overloaded names.
- RSA signature verification is verification-only and never requests a private key. X.509 input only extracts the RSA public key and displays certificate metadata; it does not validate trust, issuer, usage, validity, or a certificate chain. Raw-file, Intel HEX effective-data, and Intel HEX block-header modes are explicit and never inferred from the filename. Block-header mode uses the same binary per-block `startAddress`/`dataLength` contract and byte-order selection as Firmware Hash.
- In N/E input mode, E is an unsigned Hex integer. Odd-length input is left-padded by one zero nibble, so `10001` and `010001` both represent decimal 65537; Hex byte inputs elsewhere remain even-length.
- Browser navigation writes carrying Fetch Metadata are accepted only when `Sec-Fetch-Site` is `same-origin`. Clients that omit the header are still accepted, so this is defense in depth rather than authentication.
+8 -8
View File
@@ -4,14 +4,14 @@
一个匿名访问、自托管的浏览器本地工具箱。支持多人同时使用;除全局导航顺序外,文本、密钥、文件、图片和计算结果均只存在于各自浏览器中,不会发送到服务端。
当前应用版本:`0.3.0`。
当前应用版本:`2026.8.22`。
## 内置工具
- UTF-8 与 Hex 互转,固定路径为 `/tools/utf8-hex`
- Base64 与 Base64 URL 编码、解码,解码结果支持 UTF-8、连续 Hex 和代码字节数组
- AES-CMAC,支持 AES-128、AES-192 和 AES-256 密钥
- CRC-8/16/32/64 预设与自定义参数,支持内联 Hex 字节、BIN 和多 block Intel HEX
- 16 种 CRC-8/16/32/64 标准预设与自定义参数,支持内联 Hex 字节、BIN 和多 block Intel HEX
- 固件 SHA-256 / SHA-512,支持 Intel HEX 有效数据和带地址/长度的 Block 校验头
- 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 校验头
@@ -72,7 +72,7 @@ CHANGELOG.md 版本变更、修复和验证记录
- UTF-8 / Hex 工具的固定 ID 为 `utf8-hex`,路径为 `/tools/utf8-hex`。它按 UTF-8 字节转换文本;Hex 输入接受连续形式,或以空白、英文逗号、中文逗号分隔的片段,并允许每个片段使用可选的 `0x` 前缀。无效 UTF-8 字节必须报错,不能静默替换字符。
- 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。
- 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 的结果。
- CRC 支持内联 Hex 字节、BIN 和多 block Intel HEX,提供 16 种按位宽分组的标准预设,并允许自定义 `Width`、`Poly`、`Init`、`XorOut`、`RefIn` 和 `RefOut`。预设包括 CRC-8/SMBUS、MAXIM-DOW、SAE-J1850;CRC-16/ARC、DNP、IBM-3740、IBM-SDLC、KERMIT、MODBUS、USB、XMODEM;CRC-32/ISO-HDLC、ISCSI、MPEG-2;CRC-64/ECMA-182、XZ。界面保留常用别名:CRC-8、CCITT-FALSE、CCITT、X-25 和 CRC-32C;有歧义的 CCITT 只作为 KERMIT 后的辅助别名展示,并始终通过完整名称与 IBM-3740(CCITT-FALSE)区分。内联 Hex 接受连续形式,或以空白、英文逗号、中文逗号分隔的片段,并允许每个片段使用可选的 `0x` 前缀;空输入和无效字节格式会被拒绝,解析与计算均在独立 Web Worker 中完成。`RefIn`/`RefOut` 在页面中标为输入/输出位反转,不表示字节顺序或整个数据倒序。文件模式的总 CRC 对按地址升序拼接的有效数据计算,不填充地址空洞,同时输出每个 block 的结果。
- 固件 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 签名验证工具只执行验证,不接收私钥或生成签名。它默认选择 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 位时显示兼容性警告。
@@ -263,10 +263,10 @@ docker compose restart web-tools
## 离线镜像
若交付目录包含 `personal-web-tools-0.3.0-amd64.tar`,可直接导入:
若交付目录包含 `personal-web-tools-2026.8.22-amd64.tar`,可直接导入:
```bash
docker load -i personal-web-tools-0.3.0-amd64.tar
docker load -i personal-web-tools-2026.8.22-amd64.tar
docker compose up -d --no-build
```
@@ -320,7 +320,7 @@ npm audit --audit-level=low
`npm run check` 会依次运行全部 Vitest 测试、TypeScript/Vue 类型检查和生产构建。服务端测试使用随机本地端口验证 SPA 直达路由、静态资源 404 与缓存策略、导航 API、请求大小限制及跨源浏览器写入拦截。
自动化测试还覆盖标准算法向量、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 头部与像素限制、导航排序纯函数、目录/图标/懒加载组件一致性,以及原子写入失败后的临时文件清理。
自动化测试还覆盖标准算法向量、Base64 规范性、16 种 CRC 预设的名称/参数/Check 值、空输入与 64 位结果边界、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 头部与像素限制、导航排序纯函数、目录/图标/懒加载组件一致性,以及原子写入失败后的临时文件清理。
## 增加工具
@@ -434,9 +434,9 @@ AI 不应仅修改 `dist/`;必须修改源码并重新构建。不得覆盖或
## 版本与发布
应用版本的唯一权威来源是 `package.json`。`package-lock.json` 记录完整依赖树、下载地址和完整性摘要,供 `npm ci` 复现安装;其中项目自身的版本会在文件顶部和 `packages[""]` 中各出现一次,其余 `version` 都是第三方依赖版本,不应手工统一修改。
应用版本的唯一权威来源是 `package.json`。版本采用 npm/SemVer 兼容的日历版本 `YYYY.M.D`,例如 `2026.8.22`;月份和日期不使用前导零。每个自然日只发布一个正式版本,需要预发布验证时可使用 `YYYY.M.D-rc.N`。`package-lock.json` 记录完整依赖树、下载地址和完整性摘要,供 `npm ci` 复现安装;其中项目自身的版本会在文件顶部和 `packages[""]` 中各出现一次,其余 `version` 都是第三方依赖版本,不应手工统一修改。
使用 `npm version patch --no-git-tag-version`(或按兼容性选择 minor/major)可以同步更新 `package.json` 与 `package-lock.json`。按当前工程结构,一次完整版本发布需要更新以下 5 个已跟踪文件:
使用 `npm version YYYY.M.D --no-git-tag-version` 可以同步更新 `package.json` 与 `package-lock.json`。按当前工程结构,一次完整版本发布需要更新以下 5 个已跟踪文件:
1. `package.json`
2. `package-lock.json`
+1 -1
View File
@@ -1,6 +1,6 @@
services:
web-tools:
image: personal-web-tools:0.3.0
image: personal-web-tools:2026.8.22
build:
context: .
dockerfile: Dockerfile
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "personal-web-tools",
"version": "0.3.0",
"version": "2026.8.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "personal-web-tools",
"version": "0.3.0",
"version": "2026.8.22",
"dependencies": {
"@lucide/vue": "^1.29.0",
"@stablelib/aes": "^2.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "personal-web-tools",
"version": "0.3.0",
"version": "2026.8.22",
"private": true,
"type": "module",
"engines": {
+30 -8
View File
@@ -13,14 +13,36 @@ export interface CrcPreset extends CrcParameters {
check: bigint
}
export const CRC_PRESETS: CrcPreset[] = [
{ id: 'crc-32-iso-hdlc', name: 'CRC-32 / ISO-HDLC', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xcbf43926n },
{ id: 'crc-16-ccitt-false', name: 'CRC-16 / CCITT-FALSE', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x29b1n },
{ id: 'crc-16-modbus', name: 'CRC-16 / MODBUS', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x4b37n },
{ id: 'crc-16-arc', name: 'CRC-16 / ARC', width: 16, polynomial: 0x8005n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xbb3dn },
{ id: 'crc-16-xmodem', name: 'CRC-16 / XMODEM', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x31c3n },
{ id: 'crc-8', name: 'CRC-8', width: 8, polynomial: 0x07n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0xf4n },
]
export const DEFAULT_CRC_PRESET = {
id: 'crc-32-iso-hdlc',
name: 'CRC-32/ISO-HDLC',
width: 32,
polynomial: 0x04c11db7n,
initialValue: 0xffffffffn,
inputReflected: true,
outputReflected: true,
xorOut: 0xffffffffn,
check: 0xcbf43926n,
} as const satisfies CrcPreset
export const CRC_PRESETS = [
DEFAULT_CRC_PRESET,
{ id: 'crc-32-iscsi', name: 'CRC-32/ISCSI (CRC-32C)', width: 32, polynomial: 0x1edc6f41n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xe3069283n },
{ id: 'crc-32-mpeg-2', name: 'CRC-32/MPEG-2', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x0376e6e7n },
{ id: 'crc-16-arc', name: 'CRC-16/ARC', width: 16, polynomial: 0x8005n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xbb3dn },
{ id: 'crc-16-dnp', name: 'CRC-16/DNP', width: 16, polynomial: 0x3d65n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xea82n },
{ id: 'crc-16-ccitt-false', name: 'CRC-16/IBM-3740 (CCITT-FALSE)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x29b1n },
{ id: 'crc-16-ibm-sdlc', name: 'CRC-16/IBM-SDLC (X-25)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0x906en },
{ id: 'crc-16-kermit', name: 'CRC-16/KERMIT (CCITT)', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x2189n },
{ id: 'crc-16-modbus', name: 'CRC-16/MODBUS', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x4b37n },
{ id: 'crc-16-usb', name: 'CRC-16/USB', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xb4c8n },
{ id: 'crc-16-xmodem', name: 'CRC-16/XMODEM', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x31c3n },
{ id: 'crc-8', name: 'CRC-8/SMBUS (CRC-8)', width: 8, polynomial: 0x07n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0xf4n },
{ id: 'crc-8-maxim-dow', name: 'CRC-8/MAXIM-DOW', width: 8, polynomial: 0x31n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xa1n },
{ id: 'crc-8-sae-j1850', name: 'CRC-8/SAE-J1850', width: 8, polynomial: 0x1dn, initialValue: 0xffn, inputReflected: false, outputReflected: false, xorOut: 0xffn, check: 0x4bn },
{ id: 'crc-64-ecma-182', name: 'CRC-64/ECMA-182', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x6c40df5f0b497347n },
{ id: 'crc-64-xz', name: 'CRC-64/XZ', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0xffffffffffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffffffffffn, check: 0x995dc9bbdf1939fan },
] as const satisfies readonly CrcPreset[]
function reflectBigInt(value: bigint, width: number): bigint {
let reflected = 0n
+16 -10
View File
@@ -7,7 +7,7 @@ import TextEditor from '@/components/TextEditor.vue'
import ToolPage from '@/components/ToolPage.vue'
import { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker'
import { copyText } from '@/lib/browser'
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
import { CRC_PRESETS, DEFAULT_CRC_PRESET, type CrcParameters } from '@/lib/crc'
import {
MAX_INLINE_CRC_INPUT_CHARS,
type CrcBlockResult,
@@ -29,7 +29,11 @@ interface FirmwareData {
warnings: string[]
}
const defaultPreset = CRC_PRESETS[0]
const presetWidthOrder = [32, 16, 8, 64] as const
const presetGroups = presetWidthOrder.map((presetWidth) => ({
label: `CRC-${presetWidth}`,
presets: CRC_PRESETS.filter((preset) => preset.width === presetWidth),
}))
const inputMode = ref<CrcInputMode>('file')
const inputModeOptions = [
{ value: 'hex' as const, label: 'Hex 输入' },
@@ -38,13 +42,13 @@ const inputModeOptions = [
const hexInput = ref('')
const hexResultBytes = ref<number>()
const firmware = shallowRef<FirmwareData>()
const selectedPreset = ref(defaultPreset.id)
const width = ref(defaultPreset.width)
const polynomial = ref(defaultPreset.polynomial.toString(16).toUpperCase())
const initialValue = ref(defaultPreset.initialValue.toString(16).toUpperCase())
const xorOut = ref(defaultPreset.xorOut.toString(16).toUpperCase())
const inputReflected = ref(defaultPreset.inputReflected)
const outputReflected = ref(defaultPreset.outputReflected)
const selectedPreset = ref<string>(DEFAULT_CRC_PRESET.id)
const width = ref<number>(DEFAULT_CRC_PRESET.width)
const polynomial = ref(DEFAULT_CRC_PRESET.polynomial.toString(16).toUpperCase())
const initialValue = ref(DEFAULT_CRC_PRESET.initialValue.toString(16).toUpperCase())
const xorOut = ref(DEFAULT_CRC_PRESET.xorOut.toString(16).toUpperCase())
const inputReflected = ref<boolean>(DEFAULT_CRC_PRESET.inputReflected)
const outputReflected = ref<boolean>(DEFAULT_CRC_PRESET.outputReflected)
const combinedResult = ref('')
const blockResults = ref<CrcBlockResult[]>([])
const error = ref('')
@@ -307,7 +311,9 @@ onBeforeUnmount(() => {
<div class="crc-config">
<label class="parameter-field preset-field"><span>参数预设(Preset)</span>
<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>
<optgroup v-for="group in presetGroups" :key="group.label" :label="group.label">
<option v-for="preset in group.presets" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
</optgroup>
<option value="custom">自定义参数</option>
</select>
</label>
+83 -27
View File
@@ -1,5 +1,12 @@
import { describe, expect, it } from 'vitest'
import { calculateCrc, calculateCrcChunks, CRC_PRESETS, formatCrc } from '../src/lib/crc'
import {
calculateCrc,
calculateCrcChunks,
CRC_PRESETS,
DEFAULT_CRC_PRESET,
formatCrc,
type CrcPreset,
} from '../src/lib/crc'
import {
calculateCrcHexInput,
calculateCrcReport,
@@ -16,45 +23,76 @@ function intelHexRecord(address: number, data: number[]): string {
return `:${[...bytes, checksum].map((byte) => byte.toString(16).padStart(2, '0')).join('').toUpperCase()}`
}
const STANDARD_CRC_PRESETS = [
{ id: 'crc-32-iso-hdlc', name: 'CRC-32/ISO-HDLC', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xcbf43926n },
{ id: 'crc-32-iscsi', name: 'CRC-32/ISCSI (CRC-32C)', width: 32, polynomial: 0x1edc6f41n, initialValue: 0xffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffn, check: 0xe3069283n },
{ id: 'crc-32-mpeg-2', name: 'CRC-32/MPEG-2', width: 32, polynomial: 0x04c11db7n, initialValue: 0xffffffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x0376e6e7n },
{ id: 'crc-16-arc', name: 'CRC-16/ARC', width: 16, polynomial: 0x8005n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xbb3dn },
{ id: 'crc-16-dnp', name: 'CRC-16/DNP', width: 16, polynomial: 0x3d65n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xea82n },
{ id: 'crc-16-ccitt-false', name: 'CRC-16/IBM-3740 (CCITT-FALSE)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x29b1n },
{ id: 'crc-16-ibm-sdlc', name: 'CRC-16/IBM-SDLC (X-25)', width: 16, polynomial: 0x1021n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0x906en },
{ id: 'crc-16-kermit', name: 'CRC-16/KERMIT (CCITT)', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x2189n },
{ id: 'crc-16-modbus', name: 'CRC-16/MODBUS', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0x4b37n },
{ id: 'crc-16-usb', name: 'CRC-16/USB', width: 16, polynomial: 0x8005n, initialValue: 0xffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffn, check: 0xb4c8n },
{ id: 'crc-16-xmodem', name: 'CRC-16/XMODEM', width: 16, polynomial: 0x1021n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x31c3n },
{ id: 'crc-8', name: 'CRC-8/SMBUS (CRC-8)', width: 8, polynomial: 0x07n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0xf4n },
{ id: 'crc-8-maxim-dow', name: 'CRC-8/MAXIM-DOW', width: 8, polynomial: 0x31n, initialValue: 0n, inputReflected: true, outputReflected: true, xorOut: 0n, check: 0xa1n },
{ id: 'crc-8-sae-j1850', name: 'CRC-8/SAE-J1850', width: 8, polynomial: 0x1dn, initialValue: 0xffn, inputReflected: false, outputReflected: false, xorOut: 0xffn, check: 0x4bn },
{ id: 'crc-64-ecma-182', name: 'CRC-64/ECMA-182', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0n, inputReflected: false, outputReflected: false, xorOut: 0n, check: 0x6c40df5f0b497347n },
{ id: 'crc-64-xz', name: 'CRC-64/XZ', width: 64, polynomial: 0x42f0e1eba9ea3693n, initialValue: 0xffffffffffffffffn, inputReflected: true, outputReflected: true, xorOut: 0xffffffffffffffffn, check: 0x995dc9bbdf1939fan },
] as const satisfies readonly CrcPreset[]
function presetById(id: string): CrcPreset {
const preset = CRC_PRESETS.find((candidate) => candidate.id === id)
if (!preset) throw new Error(`Missing test preset: ${id}`)
return preset
}
describe('CRC', () => {
it('publishes the reviewed canonical preset names and parameters', () => {
expect(CRC_PRESETS).toEqual(STANDARD_CRC_PRESETS)
expect(DEFAULT_CRC_PRESET).toEqual(STANDARD_CRC_PRESETS[0])
expect(new Set(CRC_PRESETS.map((preset) => preset.id)).size).toBe(CRC_PRESETS.length)
expect(new Set(CRC_PRESETS.map((preset) => preset.name)).size).toBe(CRC_PRESETS.length)
expect(new Set(CRC_PRESETS.map((preset) => preset.width))).toEqual(new Set([8, 16, 32, 64]))
const parameterSignatures = CRC_PRESETS.map((preset) => [
preset.width,
preset.polynomial,
preset.initialValue,
preset.inputReflected,
preset.outputReflected,
preset.xorOut,
].join(':'))
expect(new Set(parameterSignatures).size).toBe(CRC_PRESETS.length)
})
it('matches standard check values for 123456789', () => {
const input = textToBytes('123456789')
for (const preset of CRC_PRESETS) {
for (const preset of STANDARD_CRC_PRESETS) {
expect(calculateCrc(input, preset), preset.name).toBe(preset.check)
expect(formatCrc(preset.check, preset.width)).toHaveLength(Math.ceil(preset.width / 4))
}
})
it('supports CRC-64 / ECMA-182 parameters', () => {
expect(calculateCrc(textToBytes('123456789'), {
width: 64,
polynomial: 0x42f0e1eba9ea3693n,
initialValue: 0n,
inputReflected: false,
outputReflected: false,
xorOut: 0n,
})).toBe(0x6c40df5f0b497347n)
})
it('supports reflected CRC-64 / XZ parameters', () => {
expect(calculateCrc(textToBytes('123456789'), {
width: 64,
polynomial: 0x42f0e1eba9ea3693n,
initialValue: 0xffffffffffffffffn,
inputReflected: true,
outputReflected: true,
xorOut: 0xffffffffffffffffn,
})).toBe(0x995dc9bbdf1939fan)
it('handles empty input and fixed-width result boundaries', () => {
for (const preset of STANDARD_CRC_PRESETS) {
const mask = (1n << BigInt(preset.width)) - 1n
expect(calculateCrc(new Uint8Array(), preset), preset.name).toBe((preset.initialValue ^ preset.xorOut) & mask)
}
expect(formatCrc(0n, 8)).toBe('00')
expect(formatCrc(0xffffffffffffffffn, 64)).toBe('FFFFFFFFFFFFFFFF')
})
it('calculates chunked data as one continuous byte stream', () => {
const preset = CRC_PRESETS[0]
const chunks = [textToBytes('123'), textToBytes('456'), textToBytes('789')]
for (const preset of [DEFAULT_CRC_PRESET, presetById('crc-64-xz')]) {
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 preset = DEFAULT_CRC_PRESET
const inputs = [
'A51F7ADD',
'A5 1F 7A DD',
@@ -71,8 +109,15 @@ describe('CRC', () => {
}
})
it('calculates the CCITT/KERMIT preset through the inline Hex path', () => {
expect(calculateCrcHexInput('31 32 33 34 35 36 37 38 39', presetById('crc-16-kermit'))).toEqual({
combinedResult: '2189',
totalBytes: 9,
})
})
it('rejects empty, malformed, and oversized inline Hex', () => {
const preset = CRC_PRESETS[0]
const preset = DEFAULT_CRC_PRESET
expect(() => calculateCrcHexInput(' ', preset)).toThrow('请输入 Hex 数据')
expect(() => calculateCrcHexInput('ABC', preset)).toThrow('偶数')
expect(() => calculateCrcHexInput('A5,,1F', preset)).toThrow('片段缺少')
@@ -82,15 +127,17 @@ describe('CRC', () => {
})
it('rejects malformed runtime parameters instead of coercing them', () => {
const preset = CRC_PRESETS[0]
const preset = DEFAULT_CRC_PRESET
expect(() => calculateCrc(new Uint8Array(), { ...preset, inputReflected: undefined as never })).toThrow('布尔值')
expect(() => calculateCrc(new Uint8Array(), { ...preset, polynomial: 7 as never })).toThrow('BigInt')
expect(() => formatCrc(0x100n, 8)).toThrow('超出当前位宽')
expect(() => formatCrc(0n, 7)).toThrow('位宽')
expect(() => calculateCrc(new Uint8Array(), { ...preset, width: 65 })).toThrow('位宽')
expect(() => calculateCrc(new Uint8Array(), { ...preset, polynomial: 0n })).toThrow('多项式')
})
it('reports both the combined CRC and each block CRC', () => {
const parameters = CRC_PRESETS.find((preset) => preset.id === 'crc-32-iso-hdlc')!
const parameters = DEFAULT_CRC_PRESET
const blocks = [
{ startAddress: 0x1000, data: textToBytes('1234') },
{ startAddress: 0x2000, data: textToBytes('56789') },
@@ -102,6 +149,15 @@ describe('CRC', () => {
{ startAddress: 0x2000, endAddress: 0x2004, length: 5, crc: formatCrc(calculateCrc(blocks[1].data, parameters), 32) },
])
})
it('preserves full 64-bit CRC values in block reports', () => {
const parameters = presetById('crc-64-xz')
const report = calculateCrcReport([{ startAddress: 0, data: textToBytes('123456789') }], parameters)
expect(report.combinedResult).toBe('995DC9BBDF1939FA')
expect(report.blockResults).toEqual([
{ startAddress: 0, endAddress: 8, length: 9, crc: '995DC9BBDF1939FA' },
])
})
})
describe('Intel HEX', () => {