@bbigbang/agent-node 0.1.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.
- package/dist/agentHost.js +483 -0
- package/dist/appVersion.js +14 -0
- package/dist/assetCachePaths.js +35 -0
- package/dist/attachmentInput.js +588 -0
- package/dist/attachmentMaterializer.js +230 -0
- package/dist/bigbangCli.js +17 -0
- package/dist/bigbangMessageSendDetection.js +284 -0
- package/dist/builtinSkillRoots.js +54 -0
- package/dist/claudeConfig.js +32 -0
- package/dist/claudeDirectRuntime.js +1960 -0
- package/dist/claudeSessionControls.js +78 -0
- package/dist/claudeTranscriptFs.js +147 -0
- package/dist/codexAppServerClient.js +188 -0
- package/dist/codexAppServerEnv.js +14 -0
- package/dist/codexAppServerRpc.js +273 -0
- package/dist/codexAppServerRuntime.js +3495 -0
- package/dist/codexBuiltinPrompt.js +117 -0
- package/dist/codexConversationSummarizer.js +76 -0
- package/dist/codexTranscriptFs.js +145 -0
- package/dist/config.js +129 -0
- package/dist/connection.js +151 -0
- package/dist/dispatchQueueStore.js +39 -0
- package/dist/dreamEnv.js +1 -0
- package/dist/dreamMemoryFallback.js +118 -0
- package/dist/dreamToolPolicy.js +293 -0
- package/dist/droidMissionRunner.js +808 -0
- package/dist/executor.js +1078 -0
- package/dist/hostRuntime.js +1 -0
- package/dist/libraryAuthorityFs.js +74 -0
- package/dist/libraryMirror.js +183 -0
- package/dist/main.js +1659 -0
- package/dist/native-worker/native-worker.mjs +475 -0
- package/dist/nativeMissionAgentDispatch.js +463 -0
- package/dist/nativeMissionRunner.js +461 -0
- package/dist/nativeSkillMounts.js +204 -0
- package/dist/nativeWorkerHost.js +142 -0
- package/dist/nodeSink.js +142 -0
- package/dist/panelHttpFetch.js +334 -0
- package/dist/runtimeDrivers.js +62 -0
- package/dist/skillFs.js +229 -0
- package/dist/soloHost.js +165 -0
- package/dist/soloNodeSink.js +138 -0
- package/dist/terminalManager.js +254 -0
- package/dist/workspaceFs.js +1020 -0
- package/dist/workspaceGit.js +694 -0
- package/dist/workspaceInspect.js +22 -0
- package/package.json +49 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { inflateSync } from 'node:zlib';
|
|
5
|
+
import { extractAttachmentIdFromUri } from './attachmentMaterializer.js';
|
|
6
|
+
export const INLINE_TEXT_ATTACHMENT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
7
|
+
export const INLINE_IMAGE_ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
const PNG_VALIDATION_MAX_INFLATED_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
export function resolveLocalImageAttachmentPath(attachment) {
|
|
10
|
+
const localPath = resolveLocalAttachmentPath(attachment);
|
|
11
|
+
if (!localPath)
|
|
12
|
+
return null;
|
|
13
|
+
if (!isWithinInlineImageSizeLimit(localPath))
|
|
14
|
+
return null;
|
|
15
|
+
const mediaType = normalizeClaudeInlineImageMediaType(attachment?.mimeType)
|
|
16
|
+
?? inferClaudeInlineImageMediaTypeFromPath(localPath);
|
|
17
|
+
if (!mediaType)
|
|
18
|
+
return null;
|
|
19
|
+
return fileMatchesClaudeImageMediaType(localPath, mediaType) ? localPath : null;
|
|
20
|
+
}
|
|
21
|
+
export function resolveLocalAttachmentPath(attachment) {
|
|
22
|
+
const uri = normalizeOptionalString(attachment?.uri);
|
|
23
|
+
if (!uri)
|
|
24
|
+
return null;
|
|
25
|
+
let localPath = null;
|
|
26
|
+
if (uri.startsWith('file://')) {
|
|
27
|
+
try {
|
|
28
|
+
localPath = fileURLToPath(uri);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (path.isAbsolute(uri)) {
|
|
35
|
+
localPath = uri;
|
|
36
|
+
}
|
|
37
|
+
if (!localPath)
|
|
38
|
+
return null;
|
|
39
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
40
|
+
}
|
|
41
|
+
export function formatAttachmentTextReference(attachment, index, resolvedLocalPath, resolvedAttachmentId) {
|
|
42
|
+
const uri = normalizeOptionalString(attachment.uri) ?? '(missing uri)';
|
|
43
|
+
const mime = normalizeOptionalString(attachment.mimeType);
|
|
44
|
+
const attachmentId = resolvedAttachmentId ?? normalizeOptionalString(attachment.assetId) ?? extractAttachmentIdFromUri(uri);
|
|
45
|
+
const localPath = resolvedLocalPath ?? normalizeOptionalString(attachment.preferredLocalPath) ?? resolveLocalAttachmentPath(attachment);
|
|
46
|
+
const filename = normalizeOptionalString(attachment.filename);
|
|
47
|
+
if (localPath) {
|
|
48
|
+
return `Attachment ${index + 1}${filename ? ` (${filename})` : ''}: local_path="${localPath}"${mime ? ` (${mime})` : ''}. Read this file directly from disk.`;
|
|
49
|
+
}
|
|
50
|
+
if (attachmentId) {
|
|
51
|
+
return `Attachment ${index + 1}${filename ? ` (${filename})` : ''}: use view_file(attachment_id="${attachmentId}") to inspect it${mime ? ` (${mime})` : ''}.`;
|
|
52
|
+
}
|
|
53
|
+
return `Attachment ${index + 1}: ${uri}${mime ? ` (${mime})` : ''}`;
|
|
54
|
+
}
|
|
55
|
+
export function promptAlreadyReferencesAttachment(promptText, attachmentId) {
|
|
56
|
+
return promptText.includes(`attachment_id="${attachmentId}"`)
|
|
57
|
+
|| promptText.includes(`ID: ${attachmentId}`);
|
|
58
|
+
}
|
|
59
|
+
export function resolveClaudeInlineImageAttachment(attachment) {
|
|
60
|
+
const localPath = resolveLocalAttachmentPath(attachment);
|
|
61
|
+
if (!localPath)
|
|
62
|
+
return null;
|
|
63
|
+
if (!isWithinInlineImageSizeLimit(localPath))
|
|
64
|
+
return null;
|
|
65
|
+
const mediaType = normalizeClaudeInlineImageMediaType(attachment.mimeType)
|
|
66
|
+
?? inferClaudeInlineImageMediaTypeFromPath(localPath);
|
|
67
|
+
if (!mediaType)
|
|
68
|
+
return null;
|
|
69
|
+
if (!fileMatchesClaudeImageMediaType(localPath, mediaType))
|
|
70
|
+
return null;
|
|
71
|
+
return { path: localPath, mediaType };
|
|
72
|
+
}
|
|
73
|
+
function isWithinInlineImageSizeLimit(localPath) {
|
|
74
|
+
try {
|
|
75
|
+
const stat = fs.statSync(localPath);
|
|
76
|
+
return stat.isFile() && stat.size <= INLINE_IMAGE_ATTACHMENT_MAX_BYTES;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function buildInlineTextAttachmentPreview(attachment, localPath, index, maxBytes = INLINE_TEXT_ATTACHMENT_PREVIEW_MAX_BYTES) {
|
|
83
|
+
if (!isInlineTextAttachment(localPath, attachment.mimeType))
|
|
84
|
+
return null;
|
|
85
|
+
const stat = fs.statSync(localPath);
|
|
86
|
+
if (!stat.isFile())
|
|
87
|
+
return null;
|
|
88
|
+
const readBytes = Math.min(stat.size, maxBytes + 4);
|
|
89
|
+
const fileBuffer = readBytes > 0
|
|
90
|
+
? readFilePrefixSync(localPath, readBytes)
|
|
91
|
+
: Buffer.alloc(0);
|
|
92
|
+
if (!isLikelyUtf8Text(fileBuffer))
|
|
93
|
+
return null;
|
|
94
|
+
const truncated = stat.size > maxBytes;
|
|
95
|
+
const previewBuffer = truncated ? fileBuffer.subarray(0, maxBytes) : fileBuffer;
|
|
96
|
+
const previewText = trimIncompleteUtf8Suffix(previewBuffer).toString('utf8');
|
|
97
|
+
const filename = normalizeOptionalString(attachment.filename);
|
|
98
|
+
const mime = normalizeOptionalString(attachment.mimeType);
|
|
99
|
+
const sizeLabel = `${(stat.size / 1024).toFixed(1)} KB`;
|
|
100
|
+
const suffix = truncated
|
|
101
|
+
? `\n\n[truncated preview: showing first ${(maxBytes / 1024).toFixed(0)} KB of ${sizeLabel}; inspect local_path for the full file.]`
|
|
102
|
+
: '';
|
|
103
|
+
return [
|
|
104
|
+
`Attachment ${index + 1}${filename ? ` (${filename})` : ''}: local_path="${localPath}"${mime ? ` (${mime})` : ''}, ${sizeLabel}.`,
|
|
105
|
+
previewText + suffix,
|
|
106
|
+
].join('\n\n');
|
|
107
|
+
}
|
|
108
|
+
function readFilePrefixSync(localPath, byteCount) {
|
|
109
|
+
const fd = fs.openSync(localPath, 'r');
|
|
110
|
+
try {
|
|
111
|
+
const buffer = Buffer.alloc(byteCount);
|
|
112
|
+
const bytesRead = fs.readSync(fd, buffer, 0, byteCount, 0);
|
|
113
|
+
return bytesRead === byteCount ? buffer : buffer.subarray(0, bytesRead);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
fs.closeSync(fd);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isInlineTextAttachment(localPath, mimeType) {
|
|
120
|
+
const normalizedMime = normalizeOptionalString(mimeType)?.toLowerCase();
|
|
121
|
+
if (normalizedMime?.startsWith('text/'))
|
|
122
|
+
return true;
|
|
123
|
+
if (normalizedMime === 'application/json' || normalizedMime === 'application/xml')
|
|
124
|
+
return true;
|
|
125
|
+
return /\.(csv|ini|json|jsonc|log|md|rst|text|toml|txt|xml|ya?ml|[cm]?[jt]sx?|py|rb|rs|go|java|kt|lua|php|sh|sql|swift|vue|css|scss|html)$/i.test(localPath);
|
|
126
|
+
}
|
|
127
|
+
function normalizeClaudeInlineImageMediaType(mimeType) {
|
|
128
|
+
const normalized = normalizeOptionalString(mimeType)?.toLowerCase();
|
|
129
|
+
switch (normalized) {
|
|
130
|
+
case 'image/png':
|
|
131
|
+
case 'image/jpeg':
|
|
132
|
+
case 'image/gif':
|
|
133
|
+
case 'image/webp':
|
|
134
|
+
return normalized;
|
|
135
|
+
case 'image/jpg':
|
|
136
|
+
return 'image/jpeg';
|
|
137
|
+
default:
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function inferClaudeInlineImageMediaTypeFromPath(localPath) {
|
|
142
|
+
const ext = path.extname(localPath).toLowerCase();
|
|
143
|
+
switch (ext) {
|
|
144
|
+
case '.png':
|
|
145
|
+
return 'image/png';
|
|
146
|
+
case '.jpg':
|
|
147
|
+
case '.jpeg':
|
|
148
|
+
return 'image/jpeg';
|
|
149
|
+
case '.gif':
|
|
150
|
+
return 'image/gif';
|
|
151
|
+
case '.webp':
|
|
152
|
+
return 'image/webp';
|
|
153
|
+
default:
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function fileMatchesClaudeImageMediaType(localPath, mediaType) {
|
|
158
|
+
let fileBuffer;
|
|
159
|
+
try {
|
|
160
|
+
fileBuffer = fs.readFileSync(localPath);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
switch (mediaType) {
|
|
166
|
+
case 'image/png':
|
|
167
|
+
return isStructurallyValidPng(fileBuffer);
|
|
168
|
+
case 'image/jpeg':
|
|
169
|
+
return isStructurallyValidJpeg(fileBuffer);
|
|
170
|
+
case 'image/gif':
|
|
171
|
+
return isStructurallyValidGif(fileBuffer);
|
|
172
|
+
case 'image/webp':
|
|
173
|
+
return isStructurallyValidWebp(fileBuffer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function isStructurallyValidPng(buffer) {
|
|
177
|
+
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
178
|
+
if (buffer.length < 45 || !buffer.subarray(0, 8).equals(pngSignature))
|
|
179
|
+
return false;
|
|
180
|
+
let offset = 8;
|
|
181
|
+
let sawIhdr = false;
|
|
182
|
+
let sawIdat = false;
|
|
183
|
+
let expectedInflatedBytes = 0;
|
|
184
|
+
const idatChunks = [];
|
|
185
|
+
while (offset + 12 <= buffer.length) {
|
|
186
|
+
const chunkLength = buffer.readUInt32BE(offset);
|
|
187
|
+
const chunkType = buffer.subarray(offset + 4, offset + 8).toString('ascii');
|
|
188
|
+
const dataStart = offset + 8;
|
|
189
|
+
const crcEnd = dataStart + chunkLength + 4;
|
|
190
|
+
if (crcEnd > buffer.length)
|
|
191
|
+
return false;
|
|
192
|
+
if (!sawIhdr) {
|
|
193
|
+
if (chunkType !== 'IHDR' || chunkLength !== 13)
|
|
194
|
+
return false;
|
|
195
|
+
const width = buffer.readUInt32BE(dataStart);
|
|
196
|
+
const height = buffer.readUInt32BE(dataStart + 4);
|
|
197
|
+
const bitDepth = buffer[dataStart + 8];
|
|
198
|
+
const colorType = buffer[dataStart + 9];
|
|
199
|
+
const compression = buffer[dataStart + 10];
|
|
200
|
+
const filter = buffer[dataStart + 11];
|
|
201
|
+
const interlace = buffer[dataStart + 12];
|
|
202
|
+
if (width === 0 || height === 0)
|
|
203
|
+
return false;
|
|
204
|
+
if (!isValidPngBitDepthColorType(bitDepth, colorType))
|
|
205
|
+
return false;
|
|
206
|
+
if (compression !== 0 || filter !== 0 || (interlace !== 0 && interlace !== 1))
|
|
207
|
+
return false;
|
|
208
|
+
expectedInflatedBytes = estimatePngInflatedByteLength(width, height, bitDepth, colorType, interlace);
|
|
209
|
+
if (expectedInflatedBytes <= 0 || expectedInflatedBytes > PNG_VALIDATION_MAX_INFLATED_BYTES)
|
|
210
|
+
return false;
|
|
211
|
+
sawIhdr = true;
|
|
212
|
+
}
|
|
213
|
+
else if (chunkType === 'IDAT') {
|
|
214
|
+
if (chunkLength === 0)
|
|
215
|
+
return false;
|
|
216
|
+
idatChunks.push(buffer.subarray(dataStart, dataStart + chunkLength));
|
|
217
|
+
sawIdat = true;
|
|
218
|
+
}
|
|
219
|
+
else if (chunkType === 'IEND') {
|
|
220
|
+
if (chunkLength !== 0 || !sawIhdr || !sawIdat || crcEnd !== buffer.length)
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
const inflated = inflateSync(Buffer.concat(idatChunks), { maxOutputLength: expectedInflatedBytes + 1 });
|
|
224
|
+
return inflated.length === expectedInflatedBytes;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
offset = crcEnd;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
function estimatePngInflatedByteLength(width, height, bitDepth, colorType, interlace) {
|
|
235
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height))
|
|
236
|
+
return 0;
|
|
237
|
+
if (interlace === 1)
|
|
238
|
+
return estimateAdam7PngInflatedByteLength(width, height, bitDepth, colorType);
|
|
239
|
+
const bitsPerPixel = getPngBitsPerPixel(bitDepth, colorType);
|
|
240
|
+
if (bitsPerPixel <= 0)
|
|
241
|
+
return 0;
|
|
242
|
+
const rowBytes = Math.ceil((width * bitsPerPixel) / 8);
|
|
243
|
+
return height * (rowBytes + 1);
|
|
244
|
+
}
|
|
245
|
+
function estimateAdam7PngInflatedByteLength(width, height, bitDepth, colorType) {
|
|
246
|
+
const bitsPerPixel = getPngBitsPerPixel(bitDepth, colorType);
|
|
247
|
+
if (bitsPerPixel <= 0)
|
|
248
|
+
return 0;
|
|
249
|
+
const passes = [
|
|
250
|
+
{ x: 0, y: 0, dx: 8, dy: 8 },
|
|
251
|
+
{ x: 4, y: 0, dx: 8, dy: 8 },
|
|
252
|
+
{ x: 0, y: 4, dx: 4, dy: 8 },
|
|
253
|
+
{ x: 2, y: 0, dx: 4, dy: 4 },
|
|
254
|
+
{ x: 0, y: 2, dx: 2, dy: 4 },
|
|
255
|
+
{ x: 1, y: 0, dx: 2, dy: 2 },
|
|
256
|
+
{ x: 0, y: 1, dx: 1, dy: 2 },
|
|
257
|
+
];
|
|
258
|
+
let total = 0;
|
|
259
|
+
for (const pass of passes) {
|
|
260
|
+
const passWidth = pass.x >= width ? 0 : Math.floor((width - pass.x + pass.dx - 1) / pass.dx);
|
|
261
|
+
const passHeight = pass.y >= height ? 0 : Math.floor((height - pass.y + pass.dy - 1) / pass.dy);
|
|
262
|
+
if (passWidth === 0 || passHeight === 0)
|
|
263
|
+
continue;
|
|
264
|
+
total += passHeight * (Math.ceil((passWidth * bitsPerPixel) / 8) + 1);
|
|
265
|
+
}
|
|
266
|
+
return total;
|
|
267
|
+
}
|
|
268
|
+
function getPngBitsPerPixel(bitDepth, colorType) {
|
|
269
|
+
switch (colorType) {
|
|
270
|
+
case 0:
|
|
271
|
+
case 3:
|
|
272
|
+
return bitDepth;
|
|
273
|
+
case 2:
|
|
274
|
+
return bitDepth * 3;
|
|
275
|
+
case 4:
|
|
276
|
+
return bitDepth * 2;
|
|
277
|
+
case 6:
|
|
278
|
+
return bitDepth * 4;
|
|
279
|
+
default:
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isValidPngBitDepthColorType(bitDepth, colorType) {
|
|
284
|
+
switch (colorType) {
|
|
285
|
+
case 0:
|
|
286
|
+
return [1, 2, 4, 8, 16].includes(bitDepth);
|
|
287
|
+
case 2:
|
|
288
|
+
case 4:
|
|
289
|
+
case 6:
|
|
290
|
+
return [8, 16].includes(bitDepth);
|
|
291
|
+
case 3:
|
|
292
|
+
return [1, 2, 4, 8].includes(bitDepth);
|
|
293
|
+
default:
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function isStructurallyValidJpeg(buffer) {
|
|
298
|
+
if (buffer.length < 12
|
|
299
|
+
|| buffer[0] !== 0xff
|
|
300
|
+
|| buffer[1] !== 0xd8
|
|
301
|
+
|| buffer[buffer.length - 2] !== 0xff
|
|
302
|
+
|| buffer[buffer.length - 1] !== 0xd9) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
let offset = 2;
|
|
306
|
+
let sawFrame = false;
|
|
307
|
+
let sawScanData = false;
|
|
308
|
+
while (offset + 1 < buffer.length) {
|
|
309
|
+
if (buffer[offset] !== 0xff)
|
|
310
|
+
return false;
|
|
311
|
+
while (offset < buffer.length && buffer[offset] === 0xff)
|
|
312
|
+
offset += 1;
|
|
313
|
+
if (offset >= buffer.length)
|
|
314
|
+
return false;
|
|
315
|
+
const marker = buffer[offset];
|
|
316
|
+
offset += 1;
|
|
317
|
+
if (marker === 0xd9)
|
|
318
|
+
return sawFrame && sawScanData && offset === buffer.length;
|
|
319
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (offset + 2 > buffer.length)
|
|
323
|
+
return false;
|
|
324
|
+
const segmentLength = buffer.readUInt16BE(offset);
|
|
325
|
+
if (segmentLength < 2)
|
|
326
|
+
return false;
|
|
327
|
+
const dataStart = offset + 2;
|
|
328
|
+
const segmentEnd = offset + segmentLength;
|
|
329
|
+
if (segmentEnd > buffer.length)
|
|
330
|
+
return false;
|
|
331
|
+
if (isJpegStartOfFrameMarker(marker)) {
|
|
332
|
+
if (segmentLength < 8)
|
|
333
|
+
return false;
|
|
334
|
+
const height = buffer.readUInt16BE(dataStart + 1);
|
|
335
|
+
const width = buffer.readUInt16BE(dataStart + 3);
|
|
336
|
+
const components = buffer[dataStart + 5];
|
|
337
|
+
if (width === 0 || height === 0 || components === 0 || components > 4)
|
|
338
|
+
return false;
|
|
339
|
+
sawFrame = true;
|
|
340
|
+
}
|
|
341
|
+
if (marker === 0xda) {
|
|
342
|
+
if (!sawFrame)
|
|
343
|
+
return false;
|
|
344
|
+
const scan = readJpegScanData(buffer, segmentEnd);
|
|
345
|
+
if (!scan?.hadData)
|
|
346
|
+
return false;
|
|
347
|
+
sawScanData = true;
|
|
348
|
+
offset = scan.nextMarkerOffset;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
offset = segmentEnd;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
function isStructurallyValidGif(buffer) {
|
|
356
|
+
const header = buffer.subarray(0, 6).toString('ascii');
|
|
357
|
+
if (buffer.length < 14 || (header !== 'GIF87a' && header !== 'GIF89a'))
|
|
358
|
+
return false;
|
|
359
|
+
const width = buffer.readUInt16LE(6);
|
|
360
|
+
const height = buffer.readUInt16LE(8);
|
|
361
|
+
if (width === 0 || height === 0)
|
|
362
|
+
return false;
|
|
363
|
+
const packed = buffer[10];
|
|
364
|
+
const globalColorTableSize = (packed & 0x80) !== 0
|
|
365
|
+
? 3 * (2 ** ((packed & 0x07) + 1))
|
|
366
|
+
: 0;
|
|
367
|
+
let offset = 13 + globalColorTableSize;
|
|
368
|
+
let sawImage = false;
|
|
369
|
+
while (offset < buffer.length) {
|
|
370
|
+
const marker = buffer[offset];
|
|
371
|
+
offset += 1;
|
|
372
|
+
if (marker === 0x3b) {
|
|
373
|
+
return sawImage && offset === buffer.length;
|
|
374
|
+
}
|
|
375
|
+
if (marker === 0x21) {
|
|
376
|
+
if (offset >= buffer.length)
|
|
377
|
+
return false;
|
|
378
|
+
offset += 1;
|
|
379
|
+
const skipped = skipGifSubBlocks(buffer, offset);
|
|
380
|
+
if (!skipped)
|
|
381
|
+
return false;
|
|
382
|
+
offset = skipped.offset;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (marker !== 0x2c)
|
|
386
|
+
return false;
|
|
387
|
+
if (offset + 9 > buffer.length)
|
|
388
|
+
return false;
|
|
389
|
+
const imageWidth = buffer.readUInt16LE(offset + 4);
|
|
390
|
+
const imageHeight = buffer.readUInt16LE(offset + 6);
|
|
391
|
+
const imagePacked = buffer[offset + 8];
|
|
392
|
+
offset += 9;
|
|
393
|
+
const localColorTableSize = (imagePacked & 0x80) !== 0
|
|
394
|
+
? 3 * (2 ** ((imagePacked & 0x07) + 1))
|
|
395
|
+
: 0;
|
|
396
|
+
offset += localColorTableSize;
|
|
397
|
+
if (imageWidth === 0 || imageHeight === 0 || offset >= buffer.length)
|
|
398
|
+
return false;
|
|
399
|
+
offset += 1;
|
|
400
|
+
const skipped = skipGifSubBlocks(buffer, offset);
|
|
401
|
+
if (!skipped?.hadData)
|
|
402
|
+
return false;
|
|
403
|
+
offset = skipped.offset;
|
|
404
|
+
sawImage = true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
function isStructurallyValidWebp(buffer) {
|
|
409
|
+
if (buffer.length < 16)
|
|
410
|
+
return false;
|
|
411
|
+
if (buffer.subarray(0, 4).toString('ascii') !== 'RIFF')
|
|
412
|
+
return false;
|
|
413
|
+
if (buffer.subarray(8, 12).toString('ascii') !== 'WEBP')
|
|
414
|
+
return false;
|
|
415
|
+
const riffSize = buffer.readUInt32LE(4);
|
|
416
|
+
if (riffSize + 8 !== buffer.length)
|
|
417
|
+
return false;
|
|
418
|
+
let offset = 12;
|
|
419
|
+
let sawImageData = false;
|
|
420
|
+
while (offset + 8 <= buffer.length) {
|
|
421
|
+
const chunkType = buffer.subarray(offset, offset + 4).toString('ascii');
|
|
422
|
+
const chunkSize = buffer.readUInt32LE(offset + 4);
|
|
423
|
+
const dataStart = offset + 8;
|
|
424
|
+
const dataEnd = dataStart + chunkSize;
|
|
425
|
+
if (dataEnd > buffer.length)
|
|
426
|
+
return false;
|
|
427
|
+
if (chunkType === 'VP8 ') {
|
|
428
|
+
if (!isValidWebpVp8Chunk(buffer.subarray(dataStart, dataEnd)))
|
|
429
|
+
return false;
|
|
430
|
+
sawImageData = true;
|
|
431
|
+
}
|
|
432
|
+
else if (chunkType === 'VP8L') {
|
|
433
|
+
if (!isValidWebpVp8lChunk(buffer.subarray(dataStart, dataEnd)))
|
|
434
|
+
return false;
|
|
435
|
+
sawImageData = true;
|
|
436
|
+
}
|
|
437
|
+
else if (chunkType === 'VP8X') {
|
|
438
|
+
if (!isValidWebpVp8xChunk(buffer.subarray(dataStart, dataEnd)))
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
else if (chunkType === 'ANMF') {
|
|
442
|
+
if (chunkSize < 16 || !containsValidWebpImageChunk(buffer, dataStart + 16, dataEnd))
|
|
443
|
+
return false;
|
|
444
|
+
sawImageData = true;
|
|
445
|
+
}
|
|
446
|
+
offset = dataEnd + (chunkSize % 2);
|
|
447
|
+
}
|
|
448
|
+
return sawImageData && offset === buffer.length;
|
|
449
|
+
}
|
|
450
|
+
function isJpegStartOfFrameMarker(marker) {
|
|
451
|
+
return ((marker >= 0xc0 && marker <= 0xc3)
|
|
452
|
+
|| (marker >= 0xc5 && marker <= 0xc7)
|
|
453
|
+
|| (marker >= 0xc9 && marker <= 0xcb)
|
|
454
|
+
|| (marker >= 0xcd && marker <= 0xcf));
|
|
455
|
+
}
|
|
456
|
+
function readJpegScanData(buffer, startOffset) {
|
|
457
|
+
let offset = startOffset;
|
|
458
|
+
let hadData = false;
|
|
459
|
+
while (offset + 1 < buffer.length) {
|
|
460
|
+
if (buffer[offset] !== 0xff) {
|
|
461
|
+
hadData = true;
|
|
462
|
+
offset += 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const marker = buffer[offset + 1];
|
|
466
|
+
if (marker === 0x00) {
|
|
467
|
+
hadData = true;
|
|
468
|
+
offset += 2;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (marker === 0xff) {
|
|
472
|
+
offset += 1;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (marker >= 0xd0 && marker <= 0xd7) {
|
|
476
|
+
hadData = true;
|
|
477
|
+
offset += 2;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
return { nextMarkerOffset: offset, hadData };
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
function skipGifSubBlocks(buffer, offset) {
|
|
485
|
+
let cursor = offset;
|
|
486
|
+
let hadData = false;
|
|
487
|
+
while (cursor < buffer.length) {
|
|
488
|
+
const blockSize = buffer[cursor];
|
|
489
|
+
cursor += 1;
|
|
490
|
+
if (blockSize === 0)
|
|
491
|
+
return { offset: cursor, hadData };
|
|
492
|
+
if (cursor + blockSize > buffer.length)
|
|
493
|
+
return null;
|
|
494
|
+
hadData = true;
|
|
495
|
+
cursor += blockSize;
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
function isValidWebpVp8Chunk(chunk) {
|
|
500
|
+
return chunk.length >= 10
|
|
501
|
+
&& chunk[3] === 0x9d
|
|
502
|
+
&& chunk[4] === 0x01
|
|
503
|
+
&& chunk[5] === 0x2a
|
|
504
|
+
&& (chunk.readUInt16LE(6) & 0x3fff) > 0
|
|
505
|
+
&& (chunk.readUInt16LE(8) & 0x3fff) > 0;
|
|
506
|
+
}
|
|
507
|
+
function isValidWebpVp8lChunk(chunk) {
|
|
508
|
+
if (chunk.length < 5 || chunk[0] !== 0x2f)
|
|
509
|
+
return false;
|
|
510
|
+
const bits = chunk.readUInt32LE(1);
|
|
511
|
+
const width = (bits & 0x3fff) + 1;
|
|
512
|
+
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
513
|
+
return width > 0 && height > 0;
|
|
514
|
+
}
|
|
515
|
+
function isValidWebpVp8xChunk(chunk) {
|
|
516
|
+
if (chunk.length !== 10)
|
|
517
|
+
return false;
|
|
518
|
+
const width = 1 + chunk[4] + (chunk[5] << 8) + (chunk[6] << 16);
|
|
519
|
+
const height = 1 + chunk[7] + (chunk[8] << 8) + (chunk[9] << 16);
|
|
520
|
+
return width > 0 && height > 0;
|
|
521
|
+
}
|
|
522
|
+
function containsValidWebpImageChunk(buffer, startOffset, endOffset) {
|
|
523
|
+
let offset = startOffset;
|
|
524
|
+
while (offset + 8 <= endOffset) {
|
|
525
|
+
const chunkType = buffer.subarray(offset, offset + 4).toString('ascii');
|
|
526
|
+
const chunkSize = buffer.readUInt32LE(offset + 4);
|
|
527
|
+
const dataStart = offset + 8;
|
|
528
|
+
const dataEnd = dataStart + chunkSize;
|
|
529
|
+
if (dataEnd > endOffset)
|
|
530
|
+
return false;
|
|
531
|
+
if (chunkType === 'VP8 ' && isValidWebpVp8Chunk(buffer.subarray(dataStart, dataEnd))) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
if (chunkType === 'VP8L' && isValidWebpVp8lChunk(buffer.subarray(dataStart, dataEnd))) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
offset = dataEnd + (chunkSize % 2);
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
function isLikelyUtf8Text(buffer) {
|
|
542
|
+
if (buffer.length === 0)
|
|
543
|
+
return true;
|
|
544
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, INLINE_TEXT_ATTACHMENT_PREVIEW_MAX_BYTES));
|
|
545
|
+
let suspicious = 0;
|
|
546
|
+
for (const byte of sample) {
|
|
547
|
+
if (byte === 0)
|
|
548
|
+
return false;
|
|
549
|
+
if (byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) {
|
|
550
|
+
suspicious += 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (suspicious > Math.max(8, sample.length * 0.05))
|
|
554
|
+
return false;
|
|
555
|
+
const decoded = sample.toString('utf8');
|
|
556
|
+
const replacementCount = (decoded.match(/\uFFFD/g) ?? []).length;
|
|
557
|
+
return replacementCount <= Math.max(4, decoded.length * 0.01);
|
|
558
|
+
}
|
|
559
|
+
function trimIncompleteUtf8Suffix(buffer) {
|
|
560
|
+
if (buffer.length === 0)
|
|
561
|
+
return buffer;
|
|
562
|
+
let continuationBytes = 0;
|
|
563
|
+
while (continuationBytes < Math.min(3, buffer.length)
|
|
564
|
+
&& (buffer[buffer.length - 1 - continuationBytes] & 0b1100_0000) === 0b1000_0000) {
|
|
565
|
+
continuationBytes += 1;
|
|
566
|
+
}
|
|
567
|
+
const leadIndex = buffer.length - 1 - continuationBytes;
|
|
568
|
+
if (leadIndex < 0) {
|
|
569
|
+
return buffer.subarray(0, buffer.length - continuationBytes);
|
|
570
|
+
}
|
|
571
|
+
const leadByte = buffer[leadIndex];
|
|
572
|
+
const expectedLength = ((leadByte & 0b1000_0000) === 0 ? 1
|
|
573
|
+
: (leadByte & 0b1110_0000) === 0b1100_0000 ? 2
|
|
574
|
+
: (leadByte & 0b1111_0000) === 0b1110_0000 ? 3
|
|
575
|
+
: (leadByte & 0b1111_1000) === 0b1111_0000 ? 4
|
|
576
|
+
: 1);
|
|
577
|
+
const actualLength = buffer.length - leadIndex;
|
|
578
|
+
if (actualLength < expectedLength) {
|
|
579
|
+
return buffer.subarray(0, leadIndex);
|
|
580
|
+
}
|
|
581
|
+
return buffer;
|
|
582
|
+
}
|
|
583
|
+
function normalizeOptionalString(value) {
|
|
584
|
+
if (typeof value !== 'string')
|
|
585
|
+
return null;
|
|
586
|
+
const normalized = value.trim();
|
|
587
|
+
return normalized ? normalized : null;
|
|
588
|
+
}
|