mirror of
https://github.com/2234839/web-font.git
synced 2026-04-30 05:08:14 +08:00
- 修复 multipart 解析器:endDelimiter 提前检查导致循环退出 - 修复 Node.js v24 兼容:改为先读取 body 再构造 Request - 修复 HTTP keep-alive 死锁:按 Content-Length 精确读取 body - 修复服务器崩溃:connectionHandle 加 try-catch,单连接错误不影响全局 - 修复 URL 写死:webfont.shenzilong.com 改为 localhost - 复制按钮加"已复制/复制失败"反馈提示 - TEMP_MAX_FILES 改为环境变量可配置 - dev 环境默认启用临时上传和管理员上传 - CSS 代码默认展示,使用技巧不再折叠 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
/**
|
|
* 轻量 multipart/form-data 解析器,不依赖外部库
|
|
*/
|
|
export interface MultipartFile {
|
|
/** 表单字段名 */
|
|
name: string;
|
|
/** 原始文件名 */
|
|
filename: string;
|
|
/** 文件二进制数据 */
|
|
data: Uint8Array;
|
|
}
|
|
|
|
export interface MultipartParseResult {
|
|
files: MultipartFile[];
|
|
}
|
|
|
|
export function parseMultipart(contentType: string, body: ArrayBuffer): MultipartParseResult {
|
|
if (!body || body.byteLength === 0) {
|
|
return { files: [] };
|
|
}
|
|
|
|
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
if (!boundaryMatch) throw new Error("No boundary found");
|
|
const boundary = boundaryMatch[1].replace(/^"(.*)"$/, "$1");
|
|
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder("utf-8");
|
|
const bodyBytes = new Uint8Array(body);
|
|
const delimiter = encoder.encode("\r\n--" + boundary);
|
|
|
|
/** 在字节数组中查找子串位置 */
|
|
function findBytes(haystack: Uint8Array, needle: Uint8Array, offset: number): number {
|
|
for (let i = offset; i <= haystack.length - needle.length; i++) {
|
|
let match = true;
|
|
for (let j = 0; j < needle.length; j++) {
|
|
if (haystack[i + j] !== needle[j]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
const files: MultipartFile[] = [];
|
|
|
|
// 跳过起始边界 "--boundary\r\n"
|
|
const startBoundary = encoder.encode("--" + boundary + "\r\n");
|
|
let pos = 0;
|
|
const sbPos = findBytes(bodyBytes, startBoundary, pos);
|
|
if (sbPos === -1) throw new Error("Invalid multipart: no start boundary");
|
|
pos = sbPos + startBoundary.length;
|
|
|
|
while (pos < bodyBytes.length) {
|
|
// 查找 headers 和 body 的分界 "\r\n\r\n"
|
|
const headerEnd = findBytes(bodyBytes, encoder.encode("\r\n\r\n"), pos);
|
|
if (headerEnd === -1) break;
|
|
|
|
const headerText = decoder.decode(bodyBytes.slice(pos, headerEnd));
|
|
const bodyStart = headerEnd + 4;
|
|
|
|
// 查找下一个 boundary
|
|
const nextBoundary = findBytes(bodyBytes, delimiter, bodyStart);
|
|
if (nextBoundary === -1) break;
|
|
|
|
// part body 去掉末尾的 "\r\n"
|
|
const partBody = bodyBytes.slice(bodyStart, nextBoundary);
|
|
const actualBody = partBody.length >= 2 && partBody[partBody.length - 1] === 10 && partBody[partBody.length - 2] === 13
|
|
? partBody.slice(0, partBody.length - 2)
|
|
: partBody;
|
|
|
|
// 从 Content-Disposition 中解析字段名和文件名
|
|
const nameMatch = headerText.match(/name="([^"]*)"/);
|
|
const filenameMatch = headerText.match(/filename="([^"]*)"/);
|
|
|
|
if (nameMatch) {
|
|
files.push({
|
|
name: nameMatch[1],
|
|
filename: filenameMatch?.[1] ?? "",
|
|
data: actualBody,
|
|
});
|
|
}
|
|
|
|
pos = nextBoundary + delimiter.length;
|
|
|
|
// 检查 boundary 后面是否紧跟 "--"(结束标记)
|
|
if (pos + 2 <= bodyBytes.length && bodyBytes[pos] === 45 && bodyBytes[pos + 1] === 45) {
|
|
break;
|
|
}
|
|
// 跳过 boundary 后的 "\r\n"
|
|
if (pos < bodyBytes.length && bodyBytes[pos] === 13 && bodyBytes[pos + 1] === 10) {
|
|
pos += 2;
|
|
}
|
|
}
|
|
|
|
return { files };
|
|
}
|