mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-12 22:11:03 +08:00
两个分支各自演进:一侧引入格式注册表、H5 客户端与 Android 外壳,另一侧 引入漫画目录书与逐页取图。冲突集中在扫描器、metadata、library 路由和 web 客户端,逐项按「两边能力都要保留」的原则合并。 合并后修复了 4 个被冲突掩盖的真实缺陷: - scanner 同时保留了两套目录书实现,类型检查与测试均无法通过;改为统一 走格式注册表,目录书在文件遍历之前发现,其内容不再重复入库。 - /manifest 的 files 对漫画目录书返回的是文件夹路径,客户端无法逐页取图; 改为由 manifest 派生真实页路径。 - 单个文件端点只认 book_files 行,漫画目录书的每一页都 404;改为目录书 额外认领其目录下的路径,并修正路径归一化使穿越尝试仍被拒绝。 - 目录书判定只看图片数量,会把夹着一本书的目录整体吞掉;改为注册表驱动 的判定,含书籍文件的目录始终是书架。 同时修复了 web 端两套测试运行器互相无法执行对方用例的问题(vitest 与 node:test 混跑会静默跳过),以及 web 侧 5 处类型错误。 Ref: #9
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { decodeText, isValidUtf8 } from '../src/formats/text-encoding.ts';
|
|
import { gb18030Bytes, utf8 } from './helpers/fixtures.ts';
|
|
|
|
/**
|
|
* Encoding detection is the difference between a readable TXT and a screen of
|
|
* replacement characters, and it is the one place where a wrong guess is
|
|
* silently catastrophic rather than merely inconvenient.
|
|
*/
|
|
|
|
describe('UTF-8 validation', () => {
|
|
it('accepts ASCII and multi-byte UTF-8', () => {
|
|
expect(isValidUtf8(utf8('plain ascii'))).toBe(true);
|
|
expect(isValidUtf8(utf8('中文混排 with English'))).toBe(true);
|
|
expect(isValidUtf8(utf8('日本語のテキスト'))).toBe(true);
|
|
expect(isValidUtf8(utf8('emoji 🎉 and combining é'))).toBe(true);
|
|
});
|
|
|
|
it('rejects an overlong encoding, which a naive decoder would accept', () => {
|
|
// 0xC0 0xAF is an overlong encoding of '/'. Accepting it is the classic way
|
|
// a validator becomes a security hole in a path-handling context.
|
|
expect(isValidUtf8(new Uint8Array([0xc0, 0xaf]))).toBe(false);
|
|
});
|
|
|
|
it('rejects a lone continuation byte and a truncated sequence', () => {
|
|
expect(isValidUtf8(new Uint8Array([0x80]))).toBe(false);
|
|
expect(isValidUtf8(new Uint8Array([0xe4, 0xb8]))).toBe(false);
|
|
});
|
|
|
|
it('rejects a surrogate code point encoded in UTF-8', () => {
|
|
// ED A0 80 encodes U+D800, which is not a valid scalar value.
|
|
expect(isValidUtf8(new Uint8Array([0xed, 0xa0, 0x80]))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('decodeText', () => {
|
|
it('strips a UTF-8 BOM so it does not leak into the first chapter title', () => {
|
|
const bytes = new Uint8Array([0xef, 0xbb, 0xbf, ...utf8('第一章')]);
|
|
const result = decodeText(bytes);
|
|
expect(result.text).toBe('第一章');
|
|
expect(result.text.charCodeAt(0)).not.toBe(0xfeff);
|
|
expect(result.guessed).toBe(false);
|
|
});
|
|
|
|
it('detects UTF-16LE from its BOM', () => {
|
|
const le = new Uint8Array([0xff, 0xfe, 0x2d, 0x4e]);
|
|
const result = decodeText(le);
|
|
expect(result.encoding).toBe('utf-16le');
|
|
expect(result.text).toBe('中');
|
|
});
|
|
|
|
it('decodes valid UTF-8 without guessing', () => {
|
|
const result = decodeText(utf8('第三章 风起'));
|
|
expect(result.text).toBe('第三章 风起');
|
|
expect(result.guessed).toBe(false);
|
|
});
|
|
|
|
it('falls back to GB18030 when the bytes are not valid UTF-8', () => {
|
|
const bytes = gb18030Bytes('第一章 起始');
|
|
// Guard the premise: if these bytes happened to be valid UTF-8 the test
|
|
// would pass without exercising the fallback at all.
|
|
expect(isValidUtf8(bytes)).toBe(false);
|
|
const result = decodeText(bytes);
|
|
expect(result.text).toBe('第一章 起始');
|
|
expect(result.encoding).toBe('gb18030');
|
|
expect(result.guessed).toBe(true);
|
|
});
|
|
|
|
it('marks the result as guessed so the UI can offer an override', () => {
|
|
const result = decodeText(gb18030Bytes('测试'));
|
|
expect(result.guessed).toBe(true);
|
|
});
|
|
|
|
it('always returns something for undecodable bytes rather than throwing', () => {
|
|
const garbage = new Uint8Array([0x81, 0x82, 0x83, 0xff, 0xfe, 0x00]);
|
|
const result = decodeText(garbage);
|
|
expect(typeof result.text).toBe('string');
|
|
});
|
|
});
|