@dimina-kit/fs-core 0.2.0-dev.20260710085051
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/agent-tools.js +52 -0
- package/dist/client.js +234 -0
- package/dist/disk-mirror.js +97 -0
- package/dist/fs-core.worker.js +1001 -0
- package/dist/fs-query.worker.js +115 -0
- package/dist/sync/sync-engine.js +381 -0
- package/dist/sync/truth-port.js +1 -0
- package/dist/worker-lib/engine-shared.js +25 -0
- package/dist/worker-lib/paths.js +17 -0
- package/dist/worker-lib/rpc-types.js +1 -0
- package/dist/worker-lib/wal-codec.js +111 -0
- package/dist/zip.js +60 -0
- package/package.json +75 -0
- package/src/__checks__/types-smoke.ts +23 -0
- package/src/agent-tools.test.ts +151 -0
- package/src/agent-tools.ts +117 -0
- package/src/client.test.ts +110 -0
- package/src/client.ts +284 -0
- package/src/disk-mirror.test.ts +190 -0
- package/src/disk-mirror.ts +119 -0
- package/src/fs-core-recovery.ts +280 -0
- package/src/fs-core-write-ops.ts +402 -0
- package/src/fs-core.worker.ts +182 -0
- package/src/fs-query.worker.ts +152 -0
- package/src/import-smoke.test.ts +40 -0
- package/src/worker-lib/engine-shared.test.ts +30 -0
- package/src/worker-lib/engine-shared.ts +74 -0
- package/src/worker-lib/paths.test.ts +39 -0
- package/src/worker-lib/paths.ts +14 -0
- package/src/worker-lib/rpc-types.ts +67 -0
- package/src/worker-lib/wal-codec.test.ts +83 -0
- package/src/worker-lib/wal-codec.ts +130 -0
- package/src/zip.test.ts +124 -0
- package/src/zip.ts +60 -0
- package/sync/sync-engine.test.ts +695 -0
- package/sync/sync-engine.ts +459 -0
- package/sync/truth-port.ts +72 -0
package/src/zip.test.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeZip } from './zip.js'
|
|
3
|
+
|
|
4
|
+
const dv = (buf: Uint8Array) => new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
5
|
+
|
|
6
|
+
/** Locates the EOCD by scanning back from the end for its signature, the same
|
|
7
|
+
* way a real unzip reader would — this also guards that the record sits at
|
|
8
|
+
* the true trailing offset (no stray bytes after it). */
|
|
9
|
+
function findEocd(buf: Uint8Array): number {
|
|
10
|
+
for (let i = buf.length - 22; i >= 0; i--) {
|
|
11
|
+
if (dv(buf).getUint32(i, true) === 0x06054b50) return i
|
|
12
|
+
}
|
|
13
|
+
throw new Error('EOCD not found')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CentralEntry {
|
|
17
|
+
name: string
|
|
18
|
+
crc: number
|
|
19
|
+
size: number
|
|
20
|
+
localHeaderOffset: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Walks the central directory starting at `cdOffset`, decoding `count` entries. */
|
|
24
|
+
function readCentralDirectory(buf: Uint8Array, cdOffset: number, count: number): CentralEntry[] {
|
|
25
|
+
const dec = new TextDecoder('utf-8')
|
|
26
|
+
const entries: CentralEntry[] = []
|
|
27
|
+
let at = cdOffset
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
const v = dv(buf)
|
|
30
|
+
expect(v.getUint32(at, true)).toBe(0x02014b50)
|
|
31
|
+
const crc = v.getUint32(at + 16, true)
|
|
32
|
+
const size = v.getUint32(at + 20, true)
|
|
33
|
+
const nameLen = v.getUint16(at + 28, true)
|
|
34
|
+
const localHeaderOffset = v.getUint32(at + 42, true)
|
|
35
|
+
const name = dec.decode(buf.subarray(at + 46, at + 46 + nameLen))
|
|
36
|
+
entries.push({ name, crc, size, localHeaderOffset })
|
|
37
|
+
at += 46 + nameLen
|
|
38
|
+
}
|
|
39
|
+
return entries
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('makeZip', () => {
|
|
43
|
+
it('produces a valid EOCD with zero entries for an empty file set', () => {
|
|
44
|
+
const buf = makeZip({})
|
|
45
|
+
const eocdOffset = findEocd(buf)
|
|
46
|
+
expect(eocdOffset).toBe(buf.length - 22)
|
|
47
|
+
const v = dv(buf)
|
|
48
|
+
expect(v.getUint16(eocdOffset + 8, true)).toBe(0) // entry count this disk
|
|
49
|
+
expect(v.getUint16(eocdOffset + 10, true)).toBe(0) // entry count total
|
|
50
|
+
expect(v.getUint32(eocdOffset + 12, true)).toBe(0) // central directory size
|
|
51
|
+
expect(v.getUint32(eocdOffset + 16, true)).toBe(0) // central directory offset
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('records the correct entry count and CRC32 for a single file', () => {
|
|
55
|
+
const buf = makeZip({ 'a.txt': 'a' })
|
|
56
|
+
const eocdOffset = findEocd(buf)
|
|
57
|
+
const v = dv(buf)
|
|
58
|
+
const count = v.getUint16(eocdOffset + 10, true)
|
|
59
|
+
expect(count).toBe(1)
|
|
60
|
+
const cdOffset = v.getUint32(eocdOffset + 16, true)
|
|
61
|
+
const entries = readCentralDirectory(buf, cdOffset, count)
|
|
62
|
+
expect(entries).toHaveLength(1)
|
|
63
|
+
const entry = entries[0]!
|
|
64
|
+
expect(entry.name).toBe('a.txt')
|
|
65
|
+
expect(entry.size).toBe(1)
|
|
66
|
+
expect(entry.crc).toBe(0xe8b7be43)
|
|
67
|
+
expect(entry.localHeaderOffset).toBe(0)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('matches known CRC32 check vectors for empty, single-char, and digit-string content', () => {
|
|
71
|
+
const files = { empty: '', single: 'a', digits: '123456789' }
|
|
72
|
+
const buf = makeZip(files)
|
|
73
|
+
const eocdOffset = findEocd(buf)
|
|
74
|
+
const v = dv(buf)
|
|
75
|
+
const count = v.getUint16(eocdOffset + 10, true)
|
|
76
|
+
const cdOffset = v.getUint32(eocdOffset + 16, true)
|
|
77
|
+
const entries = readCentralDirectory(buf, cdOffset, count)
|
|
78
|
+
const byName = Object.fromEntries(entries.map((e) => [e.name, e])) as Record<string, CentralEntry>
|
|
79
|
+
expect(byName.empty!.crc).toBe(0x00000000)
|
|
80
|
+
expect(byName.single!.crc).toBe(0xe8b7be43)
|
|
81
|
+
expect(byName.digits!.crc).toBe(0xcbf43926)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('decodes a UTF-8 filename and matching content byte length correctly among multiple entries', () => {
|
|
85
|
+
const files = {
|
|
86
|
+
'readme.md': 'hello world',
|
|
87
|
+
'目录/文件.txt': '内容',
|
|
88
|
+
'empty.txt': '',
|
|
89
|
+
}
|
|
90
|
+
const buf = makeZip(files)
|
|
91
|
+
const eocdOffset = findEocd(buf)
|
|
92
|
+
const v = dv(buf)
|
|
93
|
+
const count = v.getUint16(eocdOffset + 10, true)
|
|
94
|
+
expect(count).toBe(3)
|
|
95
|
+
const cdOffset = v.getUint32(eocdOffset + 16, true)
|
|
96
|
+
const entries = readCentralDirectory(buf, cdOffset, count)
|
|
97
|
+
const names = entries.map((e) => e.name).sort()
|
|
98
|
+
expect(names).toEqual(['empty.txt', 'readme.md', '目录/文件.txt'].sort())
|
|
99
|
+
|
|
100
|
+
const utf8Entry = entries.find((e) => e.name === '目录/文件.txt')
|
|
101
|
+
expect(utf8Entry).toBeDefined()
|
|
102
|
+
// '内容' is 2 UTF-8 encoded chars, 3 bytes each = 6 bytes total.
|
|
103
|
+
expect(utf8Entry!.size).toBe(new TextEncoder().encode('内容').length)
|
|
104
|
+
|
|
105
|
+
// Local header offsets must be strictly increasing and non-overlapping,
|
|
106
|
+
// matching the order entries were appended to `parts`.
|
|
107
|
+
const offsets = entries.map((e) => e.localHeaderOffset).sort((a, b) => a - b)
|
|
108
|
+
expect(new Set(offsets).size).toBe(3)
|
|
109
|
+
expect(offsets[0]).toBe(0)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('sets the UTF-8 filename flag (0x0800) on both local and central headers', () => {
|
|
113
|
+
const buf = makeZip({ '目录/文件.txt': 'x' })
|
|
114
|
+
const v = dv(buf)
|
|
115
|
+
// Local file header starts at offset 0; flags live at offset 6.
|
|
116
|
+
expect(v.getUint32(0, true)).toBe(0x04034b50)
|
|
117
|
+
expect(v.getUint16(6, true)).toBe(0x0800)
|
|
118
|
+
|
|
119
|
+
const eocdOffset = findEocd(buf)
|
|
120
|
+
const cdOffset = v.getUint32(eocdOffset + 16, true)
|
|
121
|
+
expect(v.getUint32(cdOffset, true)).toBe(0x02014b50)
|
|
122
|
+
expect(v.getUint16(cdOffset + 8, true)).toBe(0x0800)
|
|
123
|
+
})
|
|
124
|
+
})
|
package/src/zip.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store-only ZIP 打包(P5 导出出口)—— 无压缩、无依赖,全浏览器可用。
|
|
3
|
+
* 项目导出走"另一介质"才真正提升持久等级;zip 是最低门槛的那条出口。
|
|
4
|
+
*/
|
|
5
|
+
import { crc32 } from './worker-lib/wal-codec.js'
|
|
6
|
+
|
|
7
|
+
const enc = new TextEncoder()
|
|
8
|
+
|
|
9
|
+
/** files: {relPath: string content} → Uint8Array(ZIP,UTF-8 文件名)。 */
|
|
10
|
+
export function makeZip(files: Record<string, string>): Uint8Array {
|
|
11
|
+
const parts: Uint8Array[] = []
|
|
12
|
+
const central: Uint8Array[] = []
|
|
13
|
+
let offset = 0
|
|
14
|
+
for (const [path, content] of Object.entries(files)) {
|
|
15
|
+
const name = enc.encode(path)
|
|
16
|
+
const data = enc.encode(content)
|
|
17
|
+
const crc = crc32(data)
|
|
18
|
+
const local = new Uint8Array(30 + name.length)
|
|
19
|
+
const lv = new DataView(local.buffer)
|
|
20
|
+
lv.setUint32(0, 0x04034b50, true) // local file header
|
|
21
|
+
lv.setUint16(4, 20, true) // version needed
|
|
22
|
+
lv.setUint16(6, 0x0800, true) // flags: UTF-8 names
|
|
23
|
+
lv.setUint16(8, 0, true) // method: store
|
|
24
|
+
lv.setUint32(14, crc, true)
|
|
25
|
+
lv.setUint32(18, data.length, true)
|
|
26
|
+
lv.setUint32(22, data.length, true)
|
|
27
|
+
lv.setUint16(26, name.length, true)
|
|
28
|
+
local.set(name, 30)
|
|
29
|
+
parts.push(local, data)
|
|
30
|
+
|
|
31
|
+
const cd = new Uint8Array(46 + name.length)
|
|
32
|
+
const cv = new DataView(cd.buffer)
|
|
33
|
+
cv.setUint32(0, 0x02014b50, true) // central directory header
|
|
34
|
+
cv.setUint16(4, 20, true)
|
|
35
|
+
cv.setUint16(6, 20, true)
|
|
36
|
+
cv.setUint16(8, 0x0800, true)
|
|
37
|
+
cv.setUint16(10, 0, true)
|
|
38
|
+
cv.setUint32(16, crc, true)
|
|
39
|
+
cv.setUint32(20, data.length, true)
|
|
40
|
+
cv.setUint32(24, data.length, true)
|
|
41
|
+
cv.setUint16(28, name.length, true)
|
|
42
|
+
cv.setUint32(42, offset, true) // local header offset
|
|
43
|
+
cd.set(name, 46)
|
|
44
|
+
central.push(cd)
|
|
45
|
+
offset += local.length + data.length
|
|
46
|
+
}
|
|
47
|
+
const cdSize = central.reduce((s, c) => s + c.length, 0)
|
|
48
|
+
const eocd = new Uint8Array(22)
|
|
49
|
+
const ev = new DataView(eocd.buffer)
|
|
50
|
+
ev.setUint32(0, 0x06054b50, true)
|
|
51
|
+
ev.setUint16(8, central.length, true)
|
|
52
|
+
ev.setUint16(10, central.length, true)
|
|
53
|
+
ev.setUint32(12, cdSize, true)
|
|
54
|
+
ev.setUint32(16, offset, true)
|
|
55
|
+
const total = offset + cdSize + 22
|
|
56
|
+
const out = new Uint8Array(total)
|
|
57
|
+
let at = 0
|
|
58
|
+
for (const p of [...parts, ...central, eocd]) { out.set(p, at); at += p.length }
|
|
59
|
+
return out
|
|
60
|
+
}
|