deel-local-cli 1.13.0 → 1.14.0

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 (54) hide show
  1. package/README.ko.md +32 -26
  2. package/README.md +32 -26
  3. package/bin/deel.js +6 -1
  4. package/package.json +3 -2
  5. package/src/acp/map.js +367 -367
  6. package/src/acp/serve.js +830 -810
  7. package/src/agent/budget.js +156 -156
  8. package/src/agent/commit.js +534 -534
  9. package/src/agent/compact.js +441 -441
  10. package/src/agent/effort.js +308 -308
  11. package/src/agent/evolve.js +263 -263
  12. package/src/agent/loop.js +1755 -1728
  13. package/src/agent/memory.js +156 -156
  14. package/src/agent/mention.js +210 -210
  15. package/src/agent/recall.js +222 -222
  16. package/src/agent/session.js +1056 -1056
  17. package/src/backend/adapter.js +1355 -1355
  18. package/src/backend/cachemark.js +223 -223
  19. package/src/backend/detect.js +327 -327
  20. package/src/backend/http.js +460 -460
  21. package/src/backend/mcp.js +407 -407
  22. package/src/backend/price.js +260 -260
  23. package/src/backend/probe.js +487 -487
  24. package/src/backend/quota.js +274 -274
  25. package/src/backend/tokens.js +59 -59
  26. package/src/backend/toolfit.js +352 -352
  27. package/src/backend/wire.js +715 -715
  28. package/src/cmdnames.js +68 -0
  29. package/src/commands.js +3116 -3184
  30. package/src/config.js +283 -283
  31. package/src/i18n/en.js +545 -545
  32. package/src/i18n/ja.js +498 -498
  33. package/src/i18n/ko.js +592 -592
  34. package/src/i18n/zh.js +498 -498
  35. package/src/oneshot.js +685 -625
  36. package/src/pack/selfpack.js +15 -2
  37. package/src/pack/sheet.en.js +16 -2
  38. package/src/pack/tar.js +154 -154
  39. package/src/pack/zip.js +235 -235
  40. package/src/plugins/manage.js +416 -416
  41. package/src/repl.js +2616 -2616
  42. package/src/safety/audit.js +148 -148
  43. package/src/safety/guard.js +714 -641
  44. package/src/safety/network.js +200 -200
  45. package/src/tools/fastgrep.js +229 -229
  46. package/src/tools/fsutil.js +312 -291
  47. package/src/tools/index.js +2590 -2560
  48. package/src/tools/jobs.js +876 -876
  49. package/src/tools/spawn.js +213 -213
  50. package/src/tools/verify.js +358 -358
  51. package/src/tools/webfetch.js +418 -418
  52. package/src/ui/export.js +233 -233
  53. package/src/ui/pick.js +115 -115
  54. package/src/ui/status.js +617 -617
package/src/pack/zip.js CHANGED
@@ -1,235 +1,235 @@
1
- // ZIP 읽기·쓰기. Node 내장 zlib 만 쓴다.
2
- //
3
- // 윈도우의 Compress-Archive 는 한글 파일 이름을 보장하지 못한다.
4
- // 여기서는 이름을 UTF-8 로 쓰고 플래그 11번 비트를 세워, 어디서 풀어도 이름이 살아 있게 한다.
5
- //
6
- // 읽기는 나중에 붙였다. xlsx 가 사실은 zip 이기 때문이다 — 엑셀 파일을 열려면
7
- // 먼저 이걸 풀어야 한다. 형식이 같으니 한자리에 둔다.
8
- import { deflateRawSync, inflateRawSync } from 'node:zlib';
9
-
10
- // --- CRC32 ---------------------------------------------------------------
11
- const TABLE = (() => {
12
- const t = new Int32Array(256);
13
- for (let i = 0; i < 256; i++) {
14
- let c = i;
15
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
16
- t[i] = c;
17
- }
18
- return t;
19
- })();
20
-
21
- export function crc32(buf) {
22
- let c = -1;
23
- for (let i = 0; i < buf.length; i++) c = TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
24
- return (c ^ -1) >>> 0;
25
- }
26
-
27
- // --- DOS 날짜·시각 --------------------------------------------------------
28
- function dosTime(d) {
29
- return ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xffff;
30
- }
31
- function dosDate(d) {
32
- return (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xffff;
33
- }
34
-
35
- const UTF8_NAMES = 0x0800; // 플래그 11번 비트 — 이름이 UTF-8 이라는 표시
36
-
37
- /**
38
- * @param {Array<{name:string, data:Buffer, mtime?:Date, mode?:number}>} entries
39
- * name 은 zip 안에서의 경로. 구분자는 항상 '/'.
40
- * mode 는 유닉스 권한(예: 0o755). 실행 파일에 필요하다.
41
- * @returns {Buffer}
42
- */
43
- export function makeZip(entries) {
44
- const locals = [];
45
- const central = [];
46
- let offset = 0;
47
-
48
- for (const e of entries) {
49
- const nameBuf = Buffer.from(e.name.replace(/\\/g, '/'), 'utf8');
50
- const raw = e.data;
51
- const crc = crc32(raw);
52
-
53
- // 압축해서 더 커지면 그냥 담는다(store).
54
- const packed = deflateRawSync(raw, { level: 9 });
55
- const useDeflate = packed.length < raw.length;
56
- const body = useDeflate ? packed : raw;
57
- const method = useDeflate ? 8 : 0;
58
-
59
- const when = e.mtime ?? new Date();
60
- const time = dosTime(when);
61
- const date = dosDate(when);
62
-
63
- const lh = Buffer.alloc(30);
64
- lh.writeUInt32LE(0x04034b50, 0);
65
- lh.writeUInt16LE(20, 4);
66
- lh.writeUInt16LE(UTF8_NAMES, 6);
67
- lh.writeUInt16LE(method, 8);
68
- lh.writeUInt16LE(time, 10);
69
- lh.writeUInt16LE(date, 12);
70
- lh.writeUInt32LE(crc, 14);
71
- lh.writeUInt32LE(body.length, 18);
72
- lh.writeUInt32LE(raw.length, 22);
73
- lh.writeUInt16LE(nameBuf.length, 26);
74
- lh.writeUInt16LE(0, 28);
75
- locals.push(lh, nameBuf, body);
76
-
77
- const ch = Buffer.alloc(46);
78
- ch.writeUInt32LE(0x02014b50, 0);
79
- ch.writeUInt16LE(0x031e, 4); // 만든 쪽: 유닉스, 버전 3.0
80
- ch.writeUInt16LE(20, 6);
81
- ch.writeUInt16LE(UTF8_NAMES, 8);
82
- ch.writeUInt16LE(method, 10);
83
- ch.writeUInt16LE(time, 12);
84
- ch.writeUInt16LE(date, 14);
85
- ch.writeUInt32LE(crc, 16);
86
- ch.writeUInt32LE(body.length, 20);
87
- ch.writeUInt32LE(raw.length, 24);
88
- ch.writeUInt16LE(nameBuf.length, 28);
89
- ch.writeUInt16LE(0, 30);
90
- ch.writeUInt16LE(0, 32);
91
- ch.writeUInt16LE(0, 34);
92
- ch.writeUInt16LE(0, 36);
93
- // 바깥 속성 위쪽 16비트에 유닉스 권한. bin 의 실행 권한이 여기 실린다.
94
- ch.writeUInt32LE(((e.mode ?? 0o644) & 0xffff) << 16, 38);
95
- ch.writeUInt32LE(offset, 42);
96
- central.push(ch, nameBuf);
97
-
98
- offset += lh.length + nameBuf.length + body.length;
99
- }
100
-
101
- const cd = Buffer.concat(central);
102
- const eocd = Buffer.alloc(22);
103
- eocd.writeUInt32LE(0x06054b50, 0);
104
- eocd.writeUInt16LE(0, 4);
105
- eocd.writeUInt16LE(0, 6);
106
- eocd.writeUInt16LE(entries.length, 8);
107
- eocd.writeUInt16LE(entries.length, 10);
108
- eocd.writeUInt32LE(cd.length, 12);
109
- eocd.writeUInt32LE(offset, 16);
110
- eocd.writeUInt16LE(0, 20);
111
-
112
- return Buffer.concat([...locals, cd, eocd]);
113
- }
114
-
115
- // --- ZIP 읽기 -------------------------------------------------------------
116
- //
117
- // 뒤에서부터 읽는다. zip 은 목록(중앙 디렉터리)이 파일 끝에 있고, 그 위치를
118
- // 알려주는 표식(EOCD)이 맨 끝에 있다. 앞에서부터 훑지 않는 이유가 이것이다.
119
-
120
- const EOCD_SIG = 0x06054b50;
121
- const CEN_SIG = 0x02014b50;
122
- const LOC_SIG = 0x04034b50;
123
-
124
- function findEocd(buf) {
125
- // 주석이 붙어 있을 수 있어서 끝에서 최대 64KB 를 뒤로 훑는다.
126
- const 끝 = Math.max(0, buf.length - 22 - 0xffff);
127
- for (let i = buf.length - 22; i >= 끝; i--) {
128
- if (buf.readUInt32LE(i) === EOCD_SIG) return i;
129
- }
130
- return -1;
131
- }
132
-
133
- /**
134
- * zip 안의 파일들을 이름 → 내용(Buffer) 으로 돌려준다.
135
- *
136
- * 필요한 것만 만들었다 — 담기(store)와 deflate 두 가지. 그게 xlsx 가 쓰는 전부다.
137
- * 모르는 것을 만나면 조용히 넘기지 않고 무엇을 못 했는지 말한다.
138
- * 조용히 넘기면 '표가 비어 있다' 로 보이고, 그때는 원인을 찾을 수 없다.
139
- *
140
- * @param {Buffer} buf
141
- * @param {{ only?: (name:string)=>boolean }} [opt] 필요한 것만 풀고 싶을 때
142
- * @returns {{ files: Map<string,Buffer>, skipped: Array<{name:string, why:string}> }}
143
- */
144
- export function readZip(buf, { only = null } = {}) {
145
- const files = new Map();
146
- const skipped = [];
147
-
148
- const eocd = findEocd(buf);
149
- if (eocd < 0) throw new Error('zip 이 아닙니다 — 끝에 있어야 할 표식을 못 찾았습니다');
150
-
151
- let 개수 = buf.readUInt16LE(eocd + 10);
152
- let 시작 = buf.readUInt32LE(eocd + 16);
153
- // 0xffff/0xffffffff 는 'ZIP64 를 보라' 는 표시다. 여기서는 다루지 않는다.
154
- // 못 다룬다고 말하는 편이 반쯤 읽어 놓고 맞다고 하는 것보다 낫다.
155
- if (개수 === 0xffff || 시작 === 0xffffffff) {
156
- throw new Error('ZIP64 형식입니다 — 이 읽기는 4GB 미만 zip 만 다룹니다');
157
- }
158
-
159
- let p = 시작;
160
- for (let i = 0; i < 개수; i++) {
161
- if (p + 46 > buf.length || buf.readUInt32LE(p) !== CEN_SIG) {
162
- throw new Error(`zip 목록이 깨졌습니다 (${i + 1}번째 항목)`);
163
- }
164
- const method = buf.readUInt16LE(p + 10);
165
- const flags = buf.readUInt16LE(p + 8);
166
- const compSize = buf.readUInt32LE(p + 20);
167
- const rawSize = buf.readUInt32LE(p + 24);
168
- const nameLen = buf.readUInt16LE(p + 28);
169
- const extraLen = buf.readUInt16LE(p + 30);
170
- const commentLen = buf.readUInt16LE(p + 32);
171
- const localAt = buf.readUInt32LE(p + 42);
172
- // 이름은 UTF-8 표시가 있으면 UTF-8, 없으면 예전 zip 관례대로 그냥 바이트다.
173
- // xlsx 는 안쪽 이름이 전부 ASCII 라 어느 쪽이든 같다.
174
- const name = buf.subarray(p + 46, p + 46 + nameLen).toString('utf8');
175
- p += 46 + nameLen + extraLen + commentLen;
176
-
177
- if (name.endsWith('/')) continue; // 폴더
178
- if (only && !only(name)) continue;
179
-
180
- // 암호가 걸린 항목은 첫 비트가 서 있다. xlsx 전체 암호와는 다른 것이지만,
181
- // 어느 쪽이든 여기서는 못 푼다.
182
- if (flags & 0x0001) { skipped.push({ name, why: '항목에 암호가 걸려 있음' }); continue; }
183
-
184
- if (localAt + 30 > buf.length || buf.readUInt32LE(localAt) !== LOC_SIG) {
185
- skipped.push({ name, why: '내용 위치가 어긋남' });
186
- continue;
187
- }
188
- const lnLen = buf.readUInt16LE(localAt + 26);
189
- const leLen = buf.readUInt16LE(localAt + 28);
190
- const at = localAt + 30 + lnLen + leLen;
191
- const body = buf.subarray(at, at + compSize);
192
-
193
- if (method === 0) {
194
- files.set(name, Buffer.from(body));
195
- } else if (method === 8) {
196
- try {
197
- /*
198
- * ── 풀기 **전에** 상한을 정한다 ──────────────────────────────
199
- *
200
- * 여기가 `inflateRawSync(body)` 한 줄이었다. 다 풀어 놓고 나서
201
- * 크기를 견줬으니, 크기가 안 맞는 것을 **알아내는 시점이 이미 늦다.**
202
- *
203
- * 작은 deflate 조각 + 수 GB 로 부푸는 내용 → 프로세스가 죽는다
204
- *
205
- * `.docx`·`.xlsx` 는 사람이 아무 데서나 받아 오는 파일이고, 우리는
206
- * 모델이 시키는 대로 그것을 연다. 즉 남이 정한 바이트로 우리 메모리를
207
- * 정하게 두고 있었다. 목록에 적힌 크기(rawSize)를 이미 읽어 뒀으면서
208
- * 쓰지 않은 것이 아까운 자리다.
209
- *
210
- * zlib 는 `maxOutputLength` 로 그 자리에서 멈춰 준다. 다 풀고 재는
211
- * 것과 달리 **메모리를 안 쓰고** 멈춘다.
212
- */
213
- const 한항목상한 = 64 * 1024 * 1024; // 이 프로그램이 여는 문서의 현실적 위쪽
214
- const 상한 = Math.min(rawSize > 0 ? rawSize : 한항목상한, 한항목상한);
215
- const out = inflateRawSync(body, { maxOutputLength: 상한 });
216
- if (rawSize && out.length !== rawSize) {
217
- skipped.push({ name, why: `푼 크기가 안 맞음 (${out.length} ≠ ${rawSize})` });
218
- continue;
219
- }
220
- files.set(name, out);
221
- } catch (err) {
222
- skipped.push({ name, why: `풀지 못함 — ${err.message}` });
223
- }
224
- } else {
225
- skipped.push({ name, why: `모르는 압축 방식 ${method}` });
226
- }
227
- }
228
-
229
- return { files, skipped };
230
- }
231
-
232
- /** 앞머리 네 바이트로 zip 인지 본다. 엑셀 암호 파일과 구분하는 데 쓴다. */
233
- export function looksZip(buf) {
234
- return buf.length >= 4 && buf.readUInt32LE(0) === LOC_SIG;
235
- }
1
+ // ZIP 읽기·쓰기. Node 내장 zlib 만 쓴다.
2
+ //
3
+ // 윈도우의 Compress-Archive 는 한글 파일 이름을 보장하지 못한다.
4
+ // 여기서는 이름을 UTF-8 로 쓰고 플래그 11번 비트를 세워, 어디서 풀어도 이름이 살아 있게 한다.
5
+ //
6
+ // 읽기는 나중에 붙였다. xlsx 가 사실은 zip 이기 때문이다 — 엑셀 파일을 열려면
7
+ // 먼저 이걸 풀어야 한다. 형식이 같으니 한자리에 둔다.
8
+ import { deflateRawSync, inflateRawSync } from 'node:zlib';
9
+
10
+ // --- CRC32 ---------------------------------------------------------------
11
+ const TABLE = (() => {
12
+ const t = new Int32Array(256);
13
+ for (let i = 0; i < 256; i++) {
14
+ let c = i;
15
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
16
+ t[i] = c;
17
+ }
18
+ return t;
19
+ })();
20
+
21
+ export function crc32(buf) {
22
+ let c = -1;
23
+ for (let i = 0; i < buf.length; i++) c = TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
24
+ return (c ^ -1) >>> 0;
25
+ }
26
+
27
+ // --- DOS 날짜·시각 --------------------------------------------------------
28
+ function dosTime(d) {
29
+ return ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xffff;
30
+ }
31
+ function dosDate(d) {
32
+ return (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xffff;
33
+ }
34
+
35
+ const UTF8_NAMES = 0x0800; // 플래그 11번 비트 — 이름이 UTF-8 이라는 표시
36
+
37
+ /**
38
+ * @param {Array<{name:string, data:Buffer, mtime?:Date, mode?:number}>} entries
39
+ * name 은 zip 안에서의 경로. 구분자는 항상 '/'.
40
+ * mode 는 유닉스 권한(예: 0o755). 실행 파일에 필요하다.
41
+ * @returns {Buffer}
42
+ */
43
+ export function makeZip(entries) {
44
+ const locals = [];
45
+ const central = [];
46
+ let offset = 0;
47
+
48
+ for (const e of entries) {
49
+ const nameBuf = Buffer.from(e.name.replace(/\\/g, '/'), 'utf8');
50
+ const raw = e.data;
51
+ const crc = crc32(raw);
52
+
53
+ // 압축해서 더 커지면 그냥 담는다(store).
54
+ const packed = deflateRawSync(raw, { level: 9 });
55
+ const useDeflate = packed.length < raw.length;
56
+ const body = useDeflate ? packed : raw;
57
+ const method = useDeflate ? 8 : 0;
58
+
59
+ const when = e.mtime ?? new Date();
60
+ const time = dosTime(when);
61
+ const date = dosDate(when);
62
+
63
+ const lh = Buffer.alloc(30);
64
+ lh.writeUInt32LE(0x04034b50, 0);
65
+ lh.writeUInt16LE(20, 4);
66
+ lh.writeUInt16LE(UTF8_NAMES, 6);
67
+ lh.writeUInt16LE(method, 8);
68
+ lh.writeUInt16LE(time, 10);
69
+ lh.writeUInt16LE(date, 12);
70
+ lh.writeUInt32LE(crc, 14);
71
+ lh.writeUInt32LE(body.length, 18);
72
+ lh.writeUInt32LE(raw.length, 22);
73
+ lh.writeUInt16LE(nameBuf.length, 26);
74
+ lh.writeUInt16LE(0, 28);
75
+ locals.push(lh, nameBuf, body);
76
+
77
+ const ch = Buffer.alloc(46);
78
+ ch.writeUInt32LE(0x02014b50, 0);
79
+ ch.writeUInt16LE(0x031e, 4); // 만든 쪽: 유닉스, 버전 3.0
80
+ ch.writeUInt16LE(20, 6);
81
+ ch.writeUInt16LE(UTF8_NAMES, 8);
82
+ ch.writeUInt16LE(method, 10);
83
+ ch.writeUInt16LE(time, 12);
84
+ ch.writeUInt16LE(date, 14);
85
+ ch.writeUInt32LE(crc, 16);
86
+ ch.writeUInt32LE(body.length, 20);
87
+ ch.writeUInt32LE(raw.length, 24);
88
+ ch.writeUInt16LE(nameBuf.length, 28);
89
+ ch.writeUInt16LE(0, 30);
90
+ ch.writeUInt16LE(0, 32);
91
+ ch.writeUInt16LE(0, 34);
92
+ ch.writeUInt16LE(0, 36);
93
+ // 바깥 속성 위쪽 16비트에 유닉스 권한. bin 의 실행 권한이 여기 실린다.
94
+ ch.writeUInt32LE(((e.mode ?? 0o644) & 0xffff) << 16, 38);
95
+ ch.writeUInt32LE(offset, 42);
96
+ central.push(ch, nameBuf);
97
+
98
+ offset += lh.length + nameBuf.length + body.length;
99
+ }
100
+
101
+ const cd = Buffer.concat(central);
102
+ const eocd = Buffer.alloc(22);
103
+ eocd.writeUInt32LE(0x06054b50, 0);
104
+ eocd.writeUInt16LE(0, 4);
105
+ eocd.writeUInt16LE(0, 6);
106
+ eocd.writeUInt16LE(entries.length, 8);
107
+ eocd.writeUInt16LE(entries.length, 10);
108
+ eocd.writeUInt32LE(cd.length, 12);
109
+ eocd.writeUInt32LE(offset, 16);
110
+ eocd.writeUInt16LE(0, 20);
111
+
112
+ return Buffer.concat([...locals, cd, eocd]);
113
+ }
114
+
115
+ // --- ZIP 읽기 -------------------------------------------------------------
116
+ //
117
+ // 뒤에서부터 읽는다. zip 은 목록(중앙 디렉터리)이 파일 끝에 있고, 그 위치를
118
+ // 알려주는 표식(EOCD)이 맨 끝에 있다. 앞에서부터 훑지 않는 이유가 이것이다.
119
+
120
+ const EOCD_SIG = 0x06054b50;
121
+ const CEN_SIG = 0x02014b50;
122
+ const LOC_SIG = 0x04034b50;
123
+
124
+ function findEocd(buf) {
125
+ // 주석이 붙어 있을 수 있어서 끝에서 최대 64KB 를 뒤로 훑는다.
126
+ const 끝 = Math.max(0, buf.length - 22 - 0xffff);
127
+ for (let i = buf.length - 22; i >= 끝; i--) {
128
+ if (buf.readUInt32LE(i) === EOCD_SIG) return i;
129
+ }
130
+ return -1;
131
+ }
132
+
133
+ /**
134
+ * zip 안의 파일들을 이름 → 내용(Buffer) 으로 돌려준다.
135
+ *
136
+ * 필요한 것만 만들었다 — 담기(store)와 deflate 두 가지. 그게 xlsx 가 쓰는 전부다.
137
+ * 모르는 것을 만나면 조용히 넘기지 않고 무엇을 못 했는지 말한다.
138
+ * 조용히 넘기면 '표가 비어 있다' 로 보이고, 그때는 원인을 찾을 수 없다.
139
+ *
140
+ * @param {Buffer} buf
141
+ * @param {{ only?: (name:string)=>boolean }} [opt] 필요한 것만 풀고 싶을 때
142
+ * @returns {{ files: Map<string,Buffer>, skipped: Array<{name:string, why:string}> }}
143
+ */
144
+ export function readZip(buf, { only = null } = {}) {
145
+ const files = new Map();
146
+ const skipped = [];
147
+
148
+ const eocd = findEocd(buf);
149
+ if (eocd < 0) throw new Error('zip 이 아닙니다 — 끝에 있어야 할 표식을 못 찾았습니다');
150
+
151
+ let 개수 = buf.readUInt16LE(eocd + 10);
152
+ let 시작 = buf.readUInt32LE(eocd + 16);
153
+ // 0xffff/0xffffffff 는 'ZIP64 를 보라' 는 표시다. 여기서는 다루지 않는다.
154
+ // 못 다룬다고 말하는 편이 반쯤 읽어 놓고 맞다고 하는 것보다 낫다.
155
+ if (개수 === 0xffff || 시작 === 0xffffffff) {
156
+ throw new Error('ZIP64 형식입니다 — 이 읽기는 4GB 미만 zip 만 다룹니다');
157
+ }
158
+
159
+ let p = 시작;
160
+ for (let i = 0; i < 개수; i++) {
161
+ if (p + 46 > buf.length || buf.readUInt32LE(p) !== CEN_SIG) {
162
+ throw new Error(`zip 목록이 깨졌습니다 (${i + 1}번째 항목)`);
163
+ }
164
+ const method = buf.readUInt16LE(p + 10);
165
+ const flags = buf.readUInt16LE(p + 8);
166
+ const compSize = buf.readUInt32LE(p + 20);
167
+ const rawSize = buf.readUInt32LE(p + 24);
168
+ const nameLen = buf.readUInt16LE(p + 28);
169
+ const extraLen = buf.readUInt16LE(p + 30);
170
+ const commentLen = buf.readUInt16LE(p + 32);
171
+ const localAt = buf.readUInt32LE(p + 42);
172
+ // 이름은 UTF-8 표시가 있으면 UTF-8, 없으면 예전 zip 관례대로 그냥 바이트다.
173
+ // xlsx 는 안쪽 이름이 전부 ASCII 라 어느 쪽이든 같다.
174
+ const name = buf.subarray(p + 46, p + 46 + nameLen).toString('utf8');
175
+ p += 46 + nameLen + extraLen + commentLen;
176
+
177
+ if (name.endsWith('/')) continue; // 폴더
178
+ if (only && !only(name)) continue;
179
+
180
+ // 암호가 걸린 항목은 첫 비트가 서 있다. xlsx 전체 암호와는 다른 것이지만,
181
+ // 어느 쪽이든 여기서는 못 푼다.
182
+ if (flags & 0x0001) { skipped.push({ name, why: '항목에 암호가 걸려 있음' }); continue; }
183
+
184
+ if (localAt + 30 > buf.length || buf.readUInt32LE(localAt) !== LOC_SIG) {
185
+ skipped.push({ name, why: '내용 위치가 어긋남' });
186
+ continue;
187
+ }
188
+ const lnLen = buf.readUInt16LE(localAt + 26);
189
+ const leLen = buf.readUInt16LE(localAt + 28);
190
+ const at = localAt + 30 + lnLen + leLen;
191
+ const body = buf.subarray(at, at + compSize);
192
+
193
+ if (method === 0) {
194
+ files.set(name, Buffer.from(body));
195
+ } else if (method === 8) {
196
+ try {
197
+ /*
198
+ * ── 풀기 **전에** 상한을 정한다 ──────────────────────────────
199
+ *
200
+ * 여기가 `inflateRawSync(body)` 한 줄이었다. 다 풀어 놓고 나서
201
+ * 크기를 견줬으니, 크기가 안 맞는 것을 **알아내는 시점이 이미 늦다.**
202
+ *
203
+ * 작은 deflate 조각 + 수 GB 로 부푸는 내용 → 프로세스가 죽는다
204
+ *
205
+ * `.docx`·`.xlsx` 는 사람이 아무 데서나 받아 오는 파일이고, 우리는
206
+ * 모델이 시키는 대로 그것을 연다. 즉 남이 정한 바이트로 우리 메모리를
207
+ * 정하게 두고 있었다. 목록에 적힌 크기(rawSize)를 이미 읽어 뒀으면서
208
+ * 쓰지 않은 것이 아까운 자리다.
209
+ *
210
+ * zlib 는 `maxOutputLength` 로 그 자리에서 멈춰 준다. 다 풀고 재는
211
+ * 것과 달리 **메모리를 안 쓰고** 멈춘다.
212
+ */
213
+ const 한항목상한 = 64 * 1024 * 1024; // 이 프로그램이 여는 문서의 현실적 위쪽
214
+ const 상한 = Math.min(rawSize > 0 ? rawSize : 한항목상한, 한항목상한);
215
+ const out = inflateRawSync(body, { maxOutputLength: 상한 });
216
+ if (rawSize && out.length !== rawSize) {
217
+ skipped.push({ name, why: `푼 크기가 안 맞음 (${out.length} ≠ ${rawSize})` });
218
+ continue;
219
+ }
220
+ files.set(name, out);
221
+ } catch (err) {
222
+ skipped.push({ name, why: `풀지 못함 — ${err.message}` });
223
+ }
224
+ } else {
225
+ skipped.push({ name, why: `모르는 압축 방식 ${method}` });
226
+ }
227
+ }
228
+
229
+ return { files, skipped };
230
+ }
231
+
232
+ /** 앞머리 네 바이트로 zip 인지 본다. 엑셀 암호 파일과 구분하는 데 쓴다. */
233
+ export function looksZip(buf) {
234
+ return buf.length >= 4 && buf.readUInt32LE(0) === LOC_SIG;
235
+ }