@myagentroam/agent 0.9.158 → 0.9.159
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.
|
@@ -36,6 +36,15 @@ export interface ModelAttemptDiagnostic {
|
|
|
36
36
|
connectionId?: string;
|
|
37
37
|
connectionReused?: boolean;
|
|
38
38
|
proxyType?: 'DIRECT' | 'SOCKS5';
|
|
39
|
+
/** Actual serialized WebSocket request size, including inline image Base64. */
|
|
40
|
+
requestBytes?: number;
|
|
41
|
+
imageCount?: number;
|
|
42
|
+
/** Decoded byte count of inline input images; external URLs do not add bytes. */
|
|
43
|
+
imageBytes?: number;
|
|
44
|
+
requestFrameCount?: number;
|
|
45
|
+
sentFrameCount?: number;
|
|
46
|
+
webSocketPhase?: 'CONNECT' | 'SEND' | 'RECEIVE';
|
|
47
|
+
webSocketCloseCode?: number;
|
|
39
48
|
durationMs?: number;
|
|
40
49
|
errorCode?: string;
|
|
41
50
|
errorMessage?: string;
|
|
@@ -25,6 +25,7 @@ interface PreparedResponsesRequest {
|
|
|
25
25
|
readonly body: ResponsesRequestBody;
|
|
26
26
|
readonly continuation: boolean;
|
|
27
27
|
}
|
|
28
|
+
type ResponsesWebSocketDiagnostic = Pick<ModelAttemptDiagnostic, 'requestBytes' | 'imageCount' | 'imageBytes' | 'requestFrameCount' | 'sentFrameCount' | 'webSocketPhase' | 'webSocketCloseCode'>;
|
|
28
29
|
export declare class OpenAiResponsesAdapter implements ModelAdapter {
|
|
29
30
|
#private;
|
|
30
31
|
constructor(configuration: MarAgentModelConfiguration);
|
|
@@ -51,7 +52,8 @@ declare class OpenAiResponsesTurnSession implements ModelTurnSession {
|
|
|
51
52
|
fullRequestBody(): ResponsesRequestBody;
|
|
52
53
|
currentTurnState(): string | undefined;
|
|
53
54
|
beginAttempt(): void;
|
|
54
|
-
|
|
55
|
+
captureWebSocketDiagnostic(diagnostic: ResponsesWebSocketDiagnostic): void;
|
|
56
|
+
connectionDiagnostic(): Pick<ModelAttemptDiagnostic, 'connectionId' | 'connectionReused' | 'proxyType'> & ResponsesWebSocketDiagnostic;
|
|
55
57
|
captureTurnState(value: unknown): void;
|
|
56
58
|
withTurnStateMetadata(body: ResponsesRequestBody): ResponsesRequestBody;
|
|
57
59
|
invalidateWebSocket(): void;
|
|
@@ -8,6 +8,7 @@ import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observ
|
|
|
8
8
|
import { connectSocks5Target, fetchModelResponse, isRetryableModelHttpStatus, isRetryableModelTransportError, modelRetryMaxAttempts, isRetryableModelRequestError, modelRetryDelay, waitForModelRetry } from './http.js';
|
|
9
9
|
import { acquireModelCredential, appendModelCredentialHeaders } from './runtime-credential.js';
|
|
10
10
|
const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
|
|
11
|
+
const WEBSOCKET_SEND_FRAME_BYTES = 1024 * 1024;
|
|
11
12
|
export const OPENAI_RESPONSES_EARLY_EOF_REASON = 'OPENAI_RESPONSES_EARLY_EOF';
|
|
12
13
|
export const OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT_REASON = 'OPENAI_RESPONSES_WEBSOCKET_CONNECTION_LIMIT';
|
|
13
14
|
const X_CODEX_TURN_STATE = 'x-codex-turn-state';
|
|
@@ -624,6 +625,7 @@ class OpenAiResponsesTurnSession {
|
|
|
624
625
|
#fullBody;
|
|
625
626
|
#completedResponseId;
|
|
626
627
|
#connectionDiagnostic;
|
|
628
|
+
#webSocketDiagnostic;
|
|
627
629
|
constructor(adapter, state, cacheLease) {
|
|
628
630
|
this.adapter = adapter;
|
|
629
631
|
this.state = state;
|
|
@@ -735,9 +737,13 @@ class OpenAiResponsesTurnSession {
|
|
|
735
737
|
}
|
|
736
738
|
beginAttempt() {
|
|
737
739
|
this.#connectionDiagnostic = undefined;
|
|
740
|
+
this.#webSocketDiagnostic = undefined;
|
|
741
|
+
}
|
|
742
|
+
captureWebSocketDiagnostic(diagnostic) {
|
|
743
|
+
this.#webSocketDiagnostic = diagnostic;
|
|
738
744
|
}
|
|
739
745
|
connectionDiagnostic() {
|
|
740
|
-
return this.#connectionDiagnostic
|
|
746
|
+
return { ...this.#connectionDiagnostic, ...this.#webSocketDiagnostic };
|
|
741
747
|
}
|
|
742
748
|
captureTurnState(value) {
|
|
743
749
|
if (this.#turnState !== undefined || typeof value !== 'string')
|
|
@@ -1010,6 +1016,10 @@ async function* parseOpenAiResponsesSse(response, signal, continuation) {
|
|
|
1010
1016
|
}
|
|
1011
1017
|
}
|
|
1012
1018
|
async function* webSocketResponseEvents(input) {
|
|
1019
|
+
const body = input.turn.withTurnStateMetadata(input.body);
|
|
1020
|
+
const payload = Buffer.from(JSON.stringify({ type: 'response.create', ...body }), 'utf8');
|
|
1021
|
+
const diagnostic = webSocketRequestDiagnostic(body, payload.length);
|
|
1022
|
+
input.turn.captureWebSocketDiagnostic(diagnostic);
|
|
1013
1023
|
const socket = await input.turn.webSocketForRequest({
|
|
1014
1024
|
configuration: input.configuration,
|
|
1015
1025
|
promptCacheKey: input.promptCacheKey,
|
|
@@ -1018,32 +1028,75 @@ async function* webSocketResponseEvents(input) {
|
|
|
1018
1028
|
continuation: input.continuation,
|
|
1019
1029
|
credentialAcquired: input.credentialAcquired
|
|
1020
1030
|
});
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
type
|
|
1034
|
-
|
|
1035
|
-
|
|
1031
|
+
const messages = webSocketMessages(socket, input.signal, (code) => {
|
|
1032
|
+
diagnostic.webSocketCloseCode = code;
|
|
1033
|
+
});
|
|
1034
|
+
try {
|
|
1035
|
+
diagnostic.webSocketPhase = 'SEND';
|
|
1036
|
+
await sendWebSocketMessage(socket, payload, input.signal, () => {
|
|
1037
|
+
diagnostic.sentFrameCount = (diagnostic.sentFrameCount ?? 0) + 1;
|
|
1038
|
+
});
|
|
1039
|
+
diagnostic.webSocketPhase = 'RECEIVE';
|
|
1040
|
+
let terminal = false;
|
|
1041
|
+
for await (const item of messages) {
|
|
1042
|
+
const event = record(item);
|
|
1043
|
+
const type = text(event.type);
|
|
1044
|
+
input.turn.captureTurnState(turnStateFromEvent(event));
|
|
1045
|
+
if (type !== 'response.metadata' && type !== 'codex.response.metadata')
|
|
1046
|
+
yield item;
|
|
1047
|
+
if (type === 'response.completed' ||
|
|
1048
|
+
type === 'response.incomplete' ||
|
|
1049
|
+
type === 'response.failed' ||
|
|
1050
|
+
type === 'error') {
|
|
1051
|
+
terminal = true;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1036
1054
|
}
|
|
1055
|
+
if (!terminal && !input.signal.aborted)
|
|
1056
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'OpenAI Responses WebSocket closed before a terminal event.', {
|
|
1057
|
+
details: {
|
|
1058
|
+
reason: OPENAI_RESPONSES_EARLY_EOF_REASON,
|
|
1059
|
+
transport: 'WEBSOCKET',
|
|
1060
|
+
continuation: input.continuation
|
|
1061
|
+
},
|
|
1062
|
+
retryable: true
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
finally {
|
|
1066
|
+
messages.dispose();
|
|
1037
1067
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1068
|
+
}
|
|
1069
|
+
function webSocketRequestDiagnostic(body, requestBytes) {
|
|
1070
|
+
let imageCount = 0;
|
|
1071
|
+
let imageBytes = 0;
|
|
1072
|
+
for (const item of body.input) {
|
|
1073
|
+
const entry = record(item);
|
|
1074
|
+
const parts = Array.isArray(entry.content)
|
|
1075
|
+
? entry.content
|
|
1076
|
+
: Array.isArray(entry.output)
|
|
1077
|
+
? entry.output
|
|
1078
|
+
: [];
|
|
1079
|
+
for (const value of parts) {
|
|
1080
|
+
const part = record(value);
|
|
1081
|
+
if (part.type !== 'input_image')
|
|
1082
|
+
continue;
|
|
1083
|
+
imageCount++;
|
|
1084
|
+
const url = text(part.image_url);
|
|
1085
|
+
if (url === undefined)
|
|
1086
|
+
continue;
|
|
1087
|
+
const marker = url.indexOf(';base64,');
|
|
1088
|
+
if (url.startsWith('data:') && marker >= 0)
|
|
1089
|
+
imageBytes += Buffer.byteLength(url.slice(marker + 8), 'base64');
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
requestBytes,
|
|
1094
|
+
imageCount,
|
|
1095
|
+
imageBytes,
|
|
1096
|
+
requestFrameCount: Math.ceil(requestBytes / WEBSOCKET_SEND_FRAME_BYTES),
|
|
1097
|
+
sentFrameCount: 0,
|
|
1098
|
+
webSocketPhase: 'CONNECT'
|
|
1099
|
+
};
|
|
1047
1100
|
}
|
|
1048
1101
|
async function waitForWebSocketOpen(socket, signal) {
|
|
1049
1102
|
if (signal.aborted) {
|
|
@@ -1101,29 +1154,55 @@ async function waitForWebSocketOpen(socket, signal) {
|
|
|
1101
1154
|
socket.once('unexpected-response', unexpectedResponse);
|
|
1102
1155
|
});
|
|
1103
1156
|
}
|
|
1104
|
-
async function sendWebSocketMessage(socket,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
cleanup()
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1157
|
+
async function sendWebSocketMessage(socket, payload, signal, frameSent) {
|
|
1158
|
+
for (let offset = 0; offset < payload.length; offset += WEBSOCKET_SEND_FRAME_BYTES) {
|
|
1159
|
+
if (signal.aborted)
|
|
1160
|
+
throw signal.reason;
|
|
1161
|
+
const end = Math.min(payload.length, offset + WEBSOCKET_SEND_FRAME_BYTES);
|
|
1162
|
+
await new Promise((resolve, reject) => {
|
|
1163
|
+
let settled = false;
|
|
1164
|
+
const cleanup = () => {
|
|
1165
|
+
settled = true;
|
|
1166
|
+
signal.removeEventListener('abort', abort);
|
|
1167
|
+
socket.off('error', error);
|
|
1168
|
+
socket.off('close', close);
|
|
1169
|
+
};
|
|
1170
|
+
const abort = () => {
|
|
1171
|
+
cleanup();
|
|
1172
|
+
socket.terminate();
|
|
1120
1173
|
reject(signal.reason);
|
|
1121
|
-
|
|
1122
|
-
|
|
1174
|
+
};
|
|
1175
|
+
const error = (cause) => {
|
|
1176
|
+
cleanup();
|
|
1177
|
+
reject(webSocketReceiveError(cause));
|
|
1178
|
+
};
|
|
1179
|
+
const close = () => error(new Error('WebSocket closed while sending the request.'));
|
|
1180
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
1181
|
+
socket.once('error', error);
|
|
1182
|
+
socket.once('close', close);
|
|
1183
|
+
try {
|
|
1184
|
+
socket.send(payload.subarray(offset, end), { binary: false, fin: end === payload.length }, (cause) => {
|
|
1185
|
+
if (settled)
|
|
1186
|
+
return;
|
|
1187
|
+
cleanup();
|
|
1188
|
+
if (cause)
|
|
1189
|
+
reject(webSocketReceiveError(cause));
|
|
1190
|
+
else if (signal.aborted)
|
|
1191
|
+
reject(signal.reason);
|
|
1192
|
+
else {
|
|
1193
|
+
frameSent();
|
|
1194
|
+
resolve();
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
catch (cause) {
|
|
1199
|
+
cleanup();
|
|
1200
|
+
reject(webSocketReceiveError(cause));
|
|
1201
|
+
}
|
|
1123
1202
|
});
|
|
1124
|
-
}
|
|
1203
|
+
}
|
|
1125
1204
|
}
|
|
1126
|
-
|
|
1205
|
+
function webSocketMessages(socket, signal, closed) {
|
|
1127
1206
|
const queue = [];
|
|
1128
1207
|
let resume;
|
|
1129
1208
|
const push = (item) => {
|
|
@@ -1136,7 +1215,10 @@ async function* webSocketMessages(socket, signal) {
|
|
|
1136
1215
|
value,
|
|
1137
1216
|
observedAt: performance.now()
|
|
1138
1217
|
});
|
|
1139
|
-
const close = () =>
|
|
1218
|
+
const close = (code) => {
|
|
1219
|
+
closed(code);
|
|
1220
|
+
push({ type: 'closed' });
|
|
1221
|
+
};
|
|
1140
1222
|
const error = (cause) => push({
|
|
1141
1223
|
type: 'error',
|
|
1142
1224
|
error: webSocketReceiveError(cause)
|
|
@@ -1153,49 +1235,57 @@ async function* webSocketMessages(socket, signal) {
|
|
|
1153
1235
|
socket.on('close', close);
|
|
1154
1236
|
socket.on('error', error);
|
|
1155
1237
|
signal.addEventListener('abort', abort, { once: true });
|
|
1156
|
-
|
|
1157
|
-
let idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1158
|
-
while (true) {
|
|
1159
|
-
if (Date.now() >= idleDeadline)
|
|
1160
|
-
throw webSocketIdleError(socket);
|
|
1161
|
-
if (queue.length === 0)
|
|
1162
|
-
await new Promise((resolve, reject) => {
|
|
1163
|
-
const timer = setTimeout(() => {
|
|
1164
|
-
resume = undefined;
|
|
1165
|
-
reject(webSocketIdleError(socket));
|
|
1166
|
-
}, Math.max(0, idleDeadline - Date.now()));
|
|
1167
|
-
resume = () => {
|
|
1168
|
-
clearTimeout(timer);
|
|
1169
|
-
resolve();
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
const item = queue.shift();
|
|
1173
|
-
if (item === undefined)
|
|
1174
|
-
continue;
|
|
1175
|
-
if (item.type === 'closed')
|
|
1176
|
-
return;
|
|
1177
|
-
if (item.type === 'error')
|
|
1178
|
-
throw item.error;
|
|
1179
|
-
const text = await webSocketMessageText(item.value);
|
|
1180
|
-
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
1181
|
-
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model response exceeded the size limit.');
|
|
1182
|
-
try {
|
|
1183
|
-
const event = JSON.parse(text);
|
|
1184
|
-
if (!isWebSocketNonProgressEvent(event))
|
|
1185
|
-
idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1186
|
-
yield observeModelStreamEvent(event, item.observedAt);
|
|
1187
|
-
}
|
|
1188
|
-
catch (cause) {
|
|
1189
|
-
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model WebSocket contained invalid JSON.', { cause });
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
finally {
|
|
1238
|
+
const dispose = () => {
|
|
1194
1239
|
signal.removeEventListener('abort', abort);
|
|
1195
1240
|
socket.off('message', message);
|
|
1196
1241
|
socket.off('close', close);
|
|
1197
1242
|
socket.off('error', error);
|
|
1198
|
-
}
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
dispose,
|
|
1246
|
+
async *[Symbol.asyncIterator]() {
|
|
1247
|
+
try {
|
|
1248
|
+
let idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1249
|
+
while (true) {
|
|
1250
|
+
if (Date.now() >= idleDeadline)
|
|
1251
|
+
throw webSocketIdleError(socket);
|
|
1252
|
+
if (queue.length === 0)
|
|
1253
|
+
await new Promise((resolve, reject) => {
|
|
1254
|
+
const timer = setTimeout(() => {
|
|
1255
|
+
resume = undefined;
|
|
1256
|
+
reject(webSocketIdleError(socket));
|
|
1257
|
+
}, Math.max(0, idleDeadline - Date.now()));
|
|
1258
|
+
resume = () => {
|
|
1259
|
+
clearTimeout(timer);
|
|
1260
|
+
resolve();
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
const item = queue.shift();
|
|
1264
|
+
if (item === undefined)
|
|
1265
|
+
continue;
|
|
1266
|
+
if (item.type === 'closed')
|
|
1267
|
+
return;
|
|
1268
|
+
if (item.type === 'error')
|
|
1269
|
+
throw item.error;
|
|
1270
|
+
const text = await webSocketMessageText(item.value);
|
|
1271
|
+
if (Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
1272
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model response exceeded the size limit.');
|
|
1273
|
+
try {
|
|
1274
|
+
const event = JSON.parse(text);
|
|
1275
|
+
if (!isWebSocketNonProgressEvent(event))
|
|
1276
|
+
idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1277
|
+
yield observeModelStreamEvent(event, item.observedAt);
|
|
1278
|
+
}
|
|
1279
|
+
catch (cause) {
|
|
1280
|
+
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model WebSocket contained invalid JSON.', { cause });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
finally {
|
|
1285
|
+
dispose();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1199
1289
|
}
|
|
1200
1290
|
function webSocketIdleError(socket) {
|
|
1201
1291
|
terminateWebSocket(socket);
|
|
@@ -27,17 +27,25 @@ export class ViewImageTool {
|
|
|
27
27
|
const path = await this.paths.resolve(input.path);
|
|
28
28
|
const handle = await open(path, 'r');
|
|
29
29
|
try {
|
|
30
|
-
const stat = await handle.stat();
|
|
30
|
+
const stat = await handle.stat({ bigint: true });
|
|
31
31
|
if (!stat.isFile())
|
|
32
32
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Path is not a regular file.');
|
|
33
|
-
if (stat.size > TOOL_EXECUTION_LIMITS.viewImageMaxBytes)
|
|
33
|
+
if (stat.size > BigInt(TOOL_EXECUTION_LIMITS.viewImageMaxBytes))
|
|
34
34
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image exceeds the ${TOOL_EXECUTION_LIMITS.viewImageMaxBytes}-byte limit.`);
|
|
35
35
|
signal?.throwIfAborted();
|
|
36
36
|
const data = await handle.readFile();
|
|
37
37
|
signal?.throwIfAborted();
|
|
38
38
|
if (data.byteLength > TOOL_EXECUTION_LIMITS.viewImageMaxBytes)
|
|
39
39
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image exceeds the ${TOOL_EXECUTION_LIMITS.viewImageMaxBytes}-byte limit.`);
|
|
40
|
+
const after = await handle.stat({ bigint: true });
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
if (stat.size !== after.size ||
|
|
43
|
+
after.size !== BigInt(data.byteLength) ||
|
|
44
|
+
stat.mtimeNs !== after.mtimeNs ||
|
|
45
|
+
stat.ctimeNs !== after.ctimeNs)
|
|
46
|
+
return incompleteImage();
|
|
40
47
|
const mimeType = detectImageMime(data);
|
|
48
|
+
validateImageContainer(data, mimeType);
|
|
41
49
|
const metadata = {
|
|
42
50
|
path: input.path,
|
|
43
51
|
mimeType,
|
|
@@ -80,3 +88,101 @@ export function detectImageMime(data) {
|
|
|
80
88
|
function invalidInput() {
|
|
81
89
|
throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'View image input is invalid.');
|
|
82
90
|
}
|
|
91
|
+
// Basic container checks catch partial writes without decoding or changing pixels.
|
|
92
|
+
function validateImageContainer(data, mimeType) {
|
|
93
|
+
if (mimeType === 'image/jpeg') {
|
|
94
|
+
let offset = 2;
|
|
95
|
+
let inScan = false;
|
|
96
|
+
let hasScan = false;
|
|
97
|
+
while (offset < data.length) {
|
|
98
|
+
if (data[offset++] !== 0xff) {
|
|
99
|
+
if (!inScan)
|
|
100
|
+
incompleteImage();
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
while (data[offset] === 0xff)
|
|
104
|
+
offset++;
|
|
105
|
+
if (offset >= data.length)
|
|
106
|
+
incompleteImage();
|
|
107
|
+
const marker = data[offset++];
|
|
108
|
+
if (marker === 0 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
109
|
+
if (!inScan)
|
|
110
|
+
incompleteImage();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (marker === 0xd9) {
|
|
114
|
+
if (!hasScan)
|
|
115
|
+
incompleteImage();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (marker === 0xd8)
|
|
119
|
+
incompleteImage();
|
|
120
|
+
if (marker === 1)
|
|
121
|
+
continue;
|
|
122
|
+
if (data.length - offset < 2)
|
|
123
|
+
incompleteImage();
|
|
124
|
+
const length = data.readUInt16BE(offset);
|
|
125
|
+
if (length < 2 || offset + length > data.length)
|
|
126
|
+
incompleteImage();
|
|
127
|
+
offset += length;
|
|
128
|
+
if (marker === 0xda)
|
|
129
|
+
hasScan = true;
|
|
130
|
+
inScan = marker === 0xda || (inScan && marker === 0xdc);
|
|
131
|
+
}
|
|
132
|
+
incompleteImage();
|
|
133
|
+
}
|
|
134
|
+
if (mimeType === 'image/webp') {
|
|
135
|
+
const end = data.readUInt32LE(4) + 8;
|
|
136
|
+
if (end > data.length || end < 20)
|
|
137
|
+
incompleteImage();
|
|
138
|
+
let offset = 12;
|
|
139
|
+
let hasPixels = false;
|
|
140
|
+
while (offset < end) {
|
|
141
|
+
if (end - offset < 8)
|
|
142
|
+
incompleteImage();
|
|
143
|
+
const length = data.readUInt32LE(offset + 4);
|
|
144
|
+
const next = offset + 8 + length + (length % 2);
|
|
145
|
+
if (next > end)
|
|
146
|
+
incompleteImage();
|
|
147
|
+
const type = data.toString('ascii', offset, offset + 4);
|
|
148
|
+
if ((type === 'VP8 ' || type === 'VP8L' || type === 'ANMF') && length > 0)
|
|
149
|
+
hasPixels = true;
|
|
150
|
+
offset = next;
|
|
151
|
+
}
|
|
152
|
+
if (!hasPixels)
|
|
153
|
+
incompleteImage();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let offset = 8;
|
|
157
|
+
let hasPixels = false;
|
|
158
|
+
while (offset < data.length) {
|
|
159
|
+
if (data.length - offset < 12)
|
|
160
|
+
incompleteImage();
|
|
161
|
+
const length = data.readUInt32BE(offset);
|
|
162
|
+
const next = offset + 12 + length;
|
|
163
|
+
if (next > data.length)
|
|
164
|
+
incompleteImage();
|
|
165
|
+
const type = data.toString('ascii', offset + 4, offset + 8);
|
|
166
|
+
if (offset === 8) {
|
|
167
|
+
if (type !== 'IHDR' ||
|
|
168
|
+
length !== 13 ||
|
|
169
|
+
data.readUInt32BE(offset + 8) === 0 ||
|
|
170
|
+
data.readUInt32BE(offset + 12) === 0)
|
|
171
|
+
incompleteImage();
|
|
172
|
+
}
|
|
173
|
+
else if (type === 'IHDR')
|
|
174
|
+
incompleteImage();
|
|
175
|
+
if (type === 'IDAT' && length > 0)
|
|
176
|
+
hasPixels = true;
|
|
177
|
+
if (type === 'IEND') {
|
|
178
|
+
if (length !== 0 || !hasPixels)
|
|
179
|
+
incompleteImage();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
offset = next;
|
|
183
|
+
}
|
|
184
|
+
incompleteImage();
|
|
185
|
+
}
|
|
186
|
+
function incompleteImage() {
|
|
187
|
+
throw new MarAgentError('MAR_AGENT_IMAGE_INCOMPLETE', 'Image file is incomplete, malformed, or changed while reading. Wait for the writer to finish and read it again.');
|
|
188
|
+
}
|