增加 CRC Hex 输入并修复依赖漏洞
This commit is contained in:
@@ -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 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.
|
||||
- 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.
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
本文档记录面向使用者和维护者的重要变更。版本号以 `package.json` 为准。
|
||||
|
||||
## 未发布
|
||||
|
||||
### Added
|
||||
|
||||
- CRC 工具增加“Hex 输入 / 文件”模式切换;内联 Hex 支持连续字节、空白、中英文逗号和逐段可选 `0x` 前缀,并在独立 Web Worker 中完成受限解析与计算。
|
||||
|
||||
### Fixed
|
||||
|
||||
- 共享 Hex 字节解析器现在拒绝连续中英文逗号形成的空片段,避免输入格式错误被静默忽略。
|
||||
|
||||
### Security
|
||||
|
||||
- 将开发工具链中的传递依赖 `nanoid` 更新到 `3.3.18`,消除自定义生成器在长度为零时可能无限循环的已知漏洞。
|
||||
|
||||
## 0.3.0 - 2026-08-13
|
||||
|
||||
### Added
|
||||
|
||||
@@ -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 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
|
||||
- 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
|
||||
@@ -39,7 +39,7 @@ See [README.zh-CN.md](README.zh-CN.md) for the exact algorithm contracts, input
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+4
-3
@@ -11,7 +11,7 @@
|
||||
- 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 预设与自定义参数,支持 BIN 和多 block Intel HEX
|
||||
- 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 支持 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。
|
||||
- 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 位时显示兼容性警告。
|
||||
@@ -98,6 +98,7 @@ CHANGELOG.md 版本变更、修复和验证记录
|
||||
| 功能 | 当前限制 | 超限行为 |
|
||||
| --- | --- | --- |
|
||||
| CRC 固件文件 | 最大 64 MiB | 首次选择时拒绝;替换时保留当前文件和结果 |
|
||||
| CRC 内联 Hex | 解析后最大 1 MiB;原始输入最多 6 MiB 字符 | 在 Worker 中解析与计算,超限或空输入直接拒绝 |
|
||||
| Intel HEX 非空记录 | 最多 500,000 条 | Worker 停止解析并显示错误 |
|
||||
| Intel HEX 数据块 | 最多 1,024 个 block | Worker 停止解析并显示错误 |
|
||||
| Intel HEX 合并后的有效数据 | 最大 64 MiB | 合并分配前拒绝,Worker 停止解析并显示错误 |
|
||||
@@ -319,7 +320,7 @@ npm audit --audit-level=low
|
||||
|
||||
`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 头部与像素限制、导航排序纯函数、目录/图标/懒加载组件一致性,以及原子写入失败后的临时文件清理。
|
||||
|
||||
## 增加工具
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -1167,9 +1167,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.17",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
|
||||
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"id": "crc",
|
||||
"title": "CRC",
|
||||
"category": "checksum",
|
||||
"description": "解析 BIN 或 Intel HEX 数据并计算 CRC。",
|
||||
"description": "输入 Hex 字节或解析 BIN、Intel HEX 数据并计算 CRC。",
|
||||
"icon": "BadgeCheck"
|
||||
},
|
||||
{
|
||||
|
||||
+5
-2
@@ -36,11 +36,13 @@ export function parseHex(input: string, options?: number | ParseHexOptions): Uin
|
||||
let tokenHasDigits = false
|
||||
let tokenPrefixSeen = false
|
||||
let sawToken = false
|
||||
let commaNeedsValue = false
|
||||
for (let index = 0; index < trimmed.length; index += 1) {
|
||||
const character = trimmed[index]
|
||||
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 (character === ',' || character === ',') commaNeedsValue = true
|
||||
tokenActive = false
|
||||
tokenHasDigits = false
|
||||
tokenPrefixSeen = false
|
||||
@@ -56,9 +58,10 @@ export function parseHex(input: string, options?: number | ParseHexOptions): Uin
|
||||
digits += 1
|
||||
tokenHasDigits = true
|
||||
sawToken = true
|
||||
commaNeedsValue = false
|
||||
}
|
||||
if (tokenActive && !tokenHasDigits) throw new Error('十六进制片段缺少数值')
|
||||
if (/[,,]$/.test(trimmed)) throw new Error('十六进制片段缺少数值')
|
||||
if (commaNeedsValue) throw new Error('十六进制片段缺少数值')
|
||||
if (digits % 2 !== 0) throw new Error('十六进制字符数量必须为偶数')
|
||||
|
||||
const byteLength = digits / 2
|
||||
|
||||
+49
-5
@@ -1,4 +1,8 @@
|
||||
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 {
|
||||
startAddress: number
|
||||
@@ -17,7 +21,14 @@ export interface CrcWorkerCalculateRequest {
|
||||
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 {
|
||||
startAddress: number
|
||||
@@ -26,25 +37,58 @@ export interface CrcBlockResult {
|
||||
crc: string
|
||||
}
|
||||
|
||||
export type CrcWorkerResponse = {
|
||||
type CrcWorkerParsedResponse = {
|
||||
type: 'parsed'
|
||||
id: number
|
||||
format: 'Intel HEX' | 'BIN'
|
||||
totalBytes: number
|
||||
blockCount: number
|
||||
warnings: string[]
|
||||
} | {
|
||||
}
|
||||
|
||||
type CrcWorkerCalculatedResponse = {
|
||||
type: 'calculated'
|
||||
id: number
|
||||
combinedResult: string
|
||||
blockResults: CrcBlockResult[]
|
||||
} | {
|
||||
}
|
||||
|
||||
type CrcWorkerCalculatedHexResponse = {
|
||||
type: 'calculated-hex'
|
||||
id: number
|
||||
combinedResult: string
|
||||
totalBytes: number
|
||||
}
|
||||
|
||||
type CrcWorkerErrorResponse<TOperation extends CrcWorkerRequest['type']> = {
|
||||
type: 'error'
|
||||
id: number
|
||||
operation: CrcWorkerRequest['type']
|
||||
operation: TOperation
|
||||
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) {
|
||||
const combinedValue = calculateCrcChunks(blocks.map((block) => block.data), parameters)
|
||||
const combinedResult = formatCrc(combinedValue, parameters.width)
|
||||
|
||||
@@ -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-spinner { animation: file-drop-spin .8s linear infinite; }
|
||||
@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; }
|
||||
.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; }
|
||||
|
||||
+142
-18
@@ -1,14 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp } from '@lucide/vue'
|
||||
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { AlertCircle, Calculator, CheckCircle2, Copy, FileUp, Trash2 } from '@lucide/vue'
|
||||
import { computed, onBeforeUnmount, ref, shallowRef, watch } from '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 { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker'
|
||||
import { copyText } from '@/lib/browser'
|
||||
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'
|
||||
|
||||
type CrcInputMode = 'hex' | 'file'
|
||||
|
||||
interface FirmwareData {
|
||||
fileName: string
|
||||
fileSize: number
|
||||
@@ -19,6 +30,13 @@ interface FirmwareData {
|
||||
}
|
||||
|
||||
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 selectedPreset = ref(defaultPreset.id)
|
||||
const width = ref(defaultPreset.width)
|
||||
@@ -31,6 +49,7 @@ const combinedResult = ref('')
|
||||
const blockResults = ref<CrcBlockResult[]>([])
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
const inlineCalculating = ref(false)
|
||||
|
||||
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() {
|
||||
invalidateCalculation()
|
||||
destroyInlineWorker()
|
||||
combinedResult.value = ''
|
||||
blockResults.value = []
|
||||
hexResultBytes.value = undefined
|
||||
success.value = ''
|
||||
}
|
||||
|
||||
@@ -91,7 +123,7 @@ const {
|
||||
calculate: calculateInWorker,
|
||||
invalidateCalculation,
|
||||
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' }),
|
||||
onParsed: (response, selected) => {
|
||||
clearResults()
|
||||
@@ -125,6 +157,8 @@ const {
|
||||
})
|
||||
|
||||
let activeCalculationName = ''
|
||||
const busy = computed(() => parsing.value || calculating.value || inlineCalculating.value)
|
||||
const isCalculating = computed(() => calculating.value || inlineCalculating.value)
|
||||
|
||||
function selectFile(selected: File) {
|
||||
if (selected.size === 0) { error.value = '文件为空'; return }
|
||||
@@ -135,12 +169,71 @@ function selectFile(selected: File) {
|
||||
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() {
|
||||
invalidateResults()
|
||||
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
|
||||
try {
|
||||
const parameters = getParameters()
|
||||
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 })
|
||||
if (startStatus === 'unavailable') throw new Error('文件数据已失效,请重新选择文件')
|
||||
} catch (reason) {
|
||||
@@ -159,12 +252,42 @@ function clearFile() {
|
||||
firmware.value = undefined
|
||||
}
|
||||
|
||||
onBeforeUnmount(destroyWorkers)
|
||||
function clearHexInput() {
|
||||
hexInput.value = ''
|
||||
invalidateResults()
|
||||
}
|
||||
|
||||
watch(inputMode, invalidateResults)
|
||||
watch(hexInput, () => {
|
||||
if (inputMode.value === 'hex') invalidateResults()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyInlineWorker()
|
||||
destroyWorkers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<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
|
||||
v-else
|
||||
input-id="crc-file"
|
||||
replacement-input-id="crc-replacement-file"
|
||||
accept=".hex,.ihx,.bin,application/octet-stream"
|
||||
@@ -183,39 +306,40 @@ onBeforeUnmount(destroyWorkers)
|
||||
|
||||
<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="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 value="custom">自定义参数</option>
|
||||
</select>
|
||||
</label>
|
||||
<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 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>初始值(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>异或输出(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>多项式(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="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="busy" spellcheck="false" @input="markCustom" /></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-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-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="busy" @change="markCustom" />输出位反转(RefOut)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<button class="action-button ghost" type="button" :disabled="!combinedResult" @click="copyResult"><Copy :size="15" />复制总 CRC</button>
|
||||
</div>
|
||||
|
||||
<div v-if="combinedResult" class="crc-results">
|
||||
<div class="crc-total"><span>总有效数据 · 按地址升序拼接,不填充空洞</span><strong>{{ combinedResult }}</strong></div>
|
||||
<div class="result-table-wrap">
|
||||
<div class="crc-total"><span>{{ inputMode === 'hex' ? `Hex 输入 · ${hexResultBytes?.toLocaleString()} 字节` : '总有效数据 · 按地址升序拼接,不填充空洞' }}</span><strong>{{ combinedResult }}</strong></div>
|
||||
<div v-if="inputMode === 'file'" class="result-table-wrap">
|
||||
<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>
|
||||
<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>
|
||||
</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-else-if="success" class="status-message success"><CheckCircle2 :size="16" />{{ success }}</div>
|
||||
</ToolPage>
|
||||
|
||||
@@ -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 type { DataBlock } from '@/lib/intelHex'
|
||||
|
||||
@@ -31,6 +31,14 @@ workerScope.onmessage = async (event) => {
|
||||
})
|
||||
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 文件')
|
||||
workerScope.postMessage({ type: 'calculated', id: request.id, ...calculateCrcReport(firmwareBlocks, request.parameters) })
|
||||
} catch (reason) {
|
||||
|
||||
@@ -28,6 +28,8 @@ describe('parseHex', () => {
|
||||
expect(() => parseHex('0011', 16)).toThrow('当前为 2 字节')
|
||||
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', () => {
|
||||
|
||||
+34
-1
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
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 { concatenateBlocks, parseIntelHex } from '../src/lib/intelHex'
|
||||
import { textToBytes } from '../src/lib/bytes'
|
||||
@@ -48,6 +53,34 @@ describe('CRC', () => {
|
||||
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', () => {
|
||||
const preset = CRC_PRESETS[0]
|
||||
expect(() => calculateCrc(new Uint8Array(), { ...preset, inputReflected: undefined as never })).toThrow('布尔值')
|
||||
|
||||
Reference in New Issue
Block a user