@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/agent-tools.js +52 -0
  4. package/dist/client.js +234 -0
  5. package/dist/disk-mirror.js +97 -0
  6. package/dist/fs-core.worker.js +1001 -0
  7. package/dist/fs-query.worker.js +115 -0
  8. package/dist/sync/sync-engine.js +381 -0
  9. package/dist/sync/truth-port.js +1 -0
  10. package/dist/worker-lib/engine-shared.js +25 -0
  11. package/dist/worker-lib/paths.js +17 -0
  12. package/dist/worker-lib/rpc-types.js +1 -0
  13. package/dist/worker-lib/wal-codec.js +111 -0
  14. package/dist/zip.js +60 -0
  15. package/package.json +75 -0
  16. package/src/__checks__/types-smoke.ts +23 -0
  17. package/src/agent-tools.test.ts +151 -0
  18. package/src/agent-tools.ts +117 -0
  19. package/src/client.test.ts +110 -0
  20. package/src/client.ts +284 -0
  21. package/src/disk-mirror.test.ts +190 -0
  22. package/src/disk-mirror.ts +119 -0
  23. package/src/fs-core-recovery.ts +280 -0
  24. package/src/fs-core-write-ops.ts +402 -0
  25. package/src/fs-core.worker.ts +182 -0
  26. package/src/fs-query.worker.ts +152 -0
  27. package/src/import-smoke.test.ts +40 -0
  28. package/src/worker-lib/engine-shared.test.ts +30 -0
  29. package/src/worker-lib/engine-shared.ts +74 -0
  30. package/src/worker-lib/paths.test.ts +39 -0
  31. package/src/worker-lib/paths.ts +14 -0
  32. package/src/worker-lib/rpc-types.ts +67 -0
  33. package/src/worker-lib/wal-codec.test.ts +83 -0
  34. package/src/worker-lib/wal-codec.ts +130 -0
  35. package/src/zip.test.ts +124 -0
  36. package/src/zip.ts +60 -0
  37. package/sync/sync-engine.test.ts +695 -0
  38. package/sync/sync-engine.ts +459 -0
  39. package/sync/truth-port.ts +72 -0
@@ -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
+ }