You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

文件压缩时出现Offset Out of Bounds错误,求技术支持

自定义JavaScript Zip函数偏移越界问题排查与修复

问题描述

实现自定义JS Zip函数时,浏览器抛出Uncaught RangeError: offset is out of bounds,错误出现在中央目录条目写入zipFile的set方法调用处。

原代码

function zip(files, zipFileName) {
    const zipData = [];
    files.forEach((file) => {
        const content = new TextEncoder().encode(file.content);
        zipData.push({
            name: file.name,
            content,
            contentLength: content.length
        });
    });
    const centralDirectory = [];
    let currentOffset = 0;
    zipData.forEach((file) => {
        centralDirectory.push({
            name: file.name,
            offset: currentOffset,
            contentLength: file.contentLength
        });
        currentOffset += file.contentLength;
    });
    const zipArray = [];
    zipData.forEach((file) => {
        const header = new Uint8Array([
            0x50, 0x4b, 0x03, 0x04, // local file header signature
            0x0A, 0x00, // version needed to extract
            0x00, 0x00, // general purpose bit flag
            0x00, 0x00, // compression method
            0x00, 0x00, 0x00, 0x00, // file modification time
            0x00, 0x00, 0x00, 0x00, // file modification date
            0x00, 0x00, 0x00, 0x00, // CRC-32
            0x00, 0x00, 0x00, 0x00, // compressed size
            0x00, 0x00, 0x00, 0x00, // uncompressed size
            file.name.length, 0x00 // file name length
        ]);
        const headerArray = new Uint8Array(header.length + file.name.length);
        headerArray.set(header);
        headerArray.set(new TextEncoder().encode(file.name), header.length);
        const content = new Uint8Array(file.content);
        const fileEntry = new Uint8Array(headerArray.length + content.length);
        fileEntry.set(headerArray);
        fileEntry.set(content, headerArray.length);
        zipArray.push(fileEntry);
    });
    const centralDirectoryArray = [];
    centralDirectory.forEach((file) => {
        const header = new Uint8Array([
            0x50, 0x4b, 0x01, 0x02, // central file header signature
            0x0A, 0x00, // version made by
            0x0A, 0x00, // version needed to extract
            0x00, 0x00, // general purpose bit flag
            0x00, 0x00, // compression method
            0x00, 0x00, 0x00, 0x00, // file modification time
            0x00, 0x00, 0x00, 0x00, // file modification date
            0x00, 0x00, 0x00, 0x00, // CRC-32
            0x00, 0x00, 0x00, 0x00, // compressed size
            0x00, 0x00, 0x00, 0x00, // uncompressed size
            file.name.length, 0x00, // file name length
            0x00, 0x00, // extra field length
            0x00, 0x00, // file comment length
            0x00, 0x00, // disk number start
            0x00, 0x00, // internal file attributes
            0x00, 0x00, 0x00, 0x00, // external file attributes
            file.currentOffset & 0xFF, // relative offset of local header (lo)
            (file.currentOffset >> 8) & 0xFF, // relative offset of local header (hi)
        ]);
        const headerArray = new Uint8Array(header.length + file.name.length);
        headerArray.set(header);
        headerArray.set(new TextEncoder().encode(file.name), header.length);
        centralDirectoryArray.push(headerArray);
    });
    const endOfCentralDirectory = new Uint8Array([
        0x50, 0x4b, 0x05, 0x06, // end of central directory signature
        0x00, 0x00, 0x00, 0x00, // number of this disk
        0x00, 0x00, 0x00, 0x00, // number of the disk with the start of the central directory
        centralDirectoryArray.length & 0xFF, // total number of entries in the central directory on this disk (lo)
        (centralDirectoryArray.length >> 8) & 0xFF, // total number of entries in the central directory on this disk (hi)
        centralDirectoryArray.length & 0xFF, // total number of entries in the central directory (lo)
        (centralDirectoryArray.length >> 8) & 0xFF, // total number of entries in the central directory (hi)
        centralDirectoryArray.reduce((acc, entry) => acc + entry.length, 0) & 0xFFFFFFFF, // size of the central directory (lo)
        ((centralDirectoryArray.reduce((acc, entry) => acc + entry.length, 0) >> 8) & 0xFFFFFFFF) & 0xFF, // size of the central directory (hi)
        (currentOffset & 0xFFFFFFFF) & 0xFF, // offset of start of central directory with respect to the starting disk number (lo)
        ((currentOffset & 0xFFFFFFFF) >> 8) & 0xFF, // offset of start of central directory with respect to the starting disk number (hi)
        0x00, 0x00 // .zip file comment length
    ]);
    const zipFile = new Uint8Array(zipArray.reduce((acc, entry) => acc + entry.length, 0) + centralDirectoryArray.reduce((acc, entry) => acc + entry.length, 0) + endOfCentralDirectory.length);
    zipArray.forEach((entry) => {
        zipFile.set(entry, currentOffset);
        currentOffset += entry.length;
    });
    centralDirectoryArray.forEach((entry) => {
        zipFile.set(entry, currentOffset);
        currentOffset += entry.length;
    });
    zipFile.set(endOfCentralDirectory, currentOffset);
    const zipBlob = new Blob([zipFile], { type: 'application/zip' });
    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(zipBlob);
    link.download = zipFileName;
    link.click();
    return zipBlob;
}

const files = [
    {
        name: 'file1.txt',
        content: 'This is the content of file 1.',
    },
    {
        name: 'file2.txt',
        content: 'This is the content of file 2.',
    }
];

// Zip the files
const zipFileName = 'Archive.zip';
zip(files, zipFileName);

错误信息

Uncaught RangeError: offset is out of bounds
at Uint8Array.set (<anonymous>)
at file.js:228:21
at Array.forEach (<anonymous>)
at File.zip (file.js:227:31)
at index.js:17:19

问题分析

  1. currentOffset未重置:生成zipFile时使用的currentOffset是之前计算文件内容总长度的变量,初始值不为0,导致写入第一个文件条目时就从错误偏移开始,后续写入中央目录时偏移超出zipFile长度范围。
  2. 中央目录偏移计算错误:最初计算centralDirectory中的offset时,只累加了文件内容长度,未包含本地文件头和文件名的长度,不符合Zip格式规范;同时中央目录条目中引用偏移时写错字段(file.currentOffset应为file.offset),且偏移仅写入2字节(Zip规范要求4字节)。
  3. EOCD字段格式错误:中央目录的大小和起始偏移在EOCD中需要4字节,但原代码仅处理了低2字节,导致格式不合法。

修复后的代码

function zip(files, zipFileName) {
    const zipData = [];
    files.forEach((file) => {
        const content = new TextEncoder().encode(file.content);
        zipData.push({
            name: file.name,
            content,
            contentLength: content.length
        });
    });

    // 计算每个文件条目的完整长度(头+文件名+内容),记录正确偏移
    const centralDirectory = [];
    let currentOffset = 0;
    zipData.forEach((file) => {
        const localHeaderLength = 30 + file.name.length;
        const entryTotalLength = localHeaderLength + file.contentLength;
        centralDirectory.push({
            name: file.name,
            offset: currentOffset,
            contentLength: file.contentLength,
            entryLength: entryTotalLength
        });
        currentOffset += entryTotalLength;
    });

    const zipArray = [];
    zipData.forEach((file) => {
        const header = new Uint8Array([
            0x50, 0x4b, 0x03, 0x04, // local file header signature
            0x0A, 0x00, // version needed to extract
            0x00, 0x00, // general purpose bit flag
            0x00, 0x00, // compression method (0 = no compression)
            0x00, 0x00, 0x00, 0x00, // file modification time
            0x00, 0x00, 0x00, 0x00, // file modification date
            0x00, 0x00, 0x00, 0x00, // CRC-32
            ...uint32ToBytes(file.contentLength), // compressed size
            ...uint32ToBytes(file.contentLength), // uncompressed size
            ...uint16ToBytes(file.name.length), // file name length
            0x00, 0x00 // extra field length
        ]);
        const headerArray = new Uint8Array(header.length + file.name.length);
        headerArray.set(header);
        headerArray.set(new TextEncoder().encode(file.name), header.length);
        const fileEntry = new Uint8Array(headerArray.length + file.content.length);
        fileEntry.set(headerArray);
        fileEntry.set(file.content, headerArray.length);
        zipArray.push(fileEntry);
    });

    const centralDirectoryArray = [];
    centralDirectory.forEach((file) => {
        const header = new Uint8Array([
            0x50, 0x4b, 0x01, 0x02, // central file header signature
            0x14, 0x00, // version made by (UTF-8 support)
            0x0A, 0x00, // version needed to extract
            0x00, 0x00, // general purpose bit flag
            0x00, 0x00, // compression method
            0x00, 0x00, 0x00, 0x00, // file modification time
            0x00, 0x00, 0x00, 0x00, // file modification date
            0x00, 0x00, 0x00, 0x00, // CRC-32
            ...uint32ToBytes(file.contentLength), // compressed size
            ...uint32ToBytes(file.contentLength), // uncompressed size
            ...uint16ToBytes(file.name.length), // file name length
            0x00, 0x00, // extra field length
            0x00, 0x00, // file comment length
            0x00, 0x00, // disk number start
            0x00, 0x00, // internal file attributes
            0x00, 0x00, 0x00, 0x00, // external file attributes
            ...uint32ToBytes(file.offset) // relative offset of local header (4 bytes)
        ]);
        const headerArray = new Uint8Array(header.length + file.name.length);
        headerArray.set(header);
        headerArray.set(new TextEncoder().encode(file.name), header.length);
        centralDirectoryArray.push(headerArray);
    });

    const centralDirTotalSize = centralDirectoryArray.reduce((acc, entry) => acc + entry.length, 0);
    const centralDirOffset = zipArray.reduce((acc, entry) => acc + entry.length, 0);
    const endOfCentralDirectory = new Uint8Array([
        0x50, 0x4b, 0x05, 0x06, // end of central directory signature
        0x00, 0x00, // number of this disk
        0x00, 0x00, // number of the disk with the start of the central directory
        ...uint16ToBytes(centralDirectoryArray.length), // total entries on this disk
        ...uint16ToBytes(centralDirectoryArray.length), // total entries in central dir
        ...uint32ToBytes(centralDirTotalSize), // size of central directory
        ...uint32ToBytes(centralDirOffset), // offset of central directory
        0x00, 0x00 // zip file comment length
    ]);

    const totalSize = zipArray.reduce((acc, entry) => acc + entry.length, 0) + centralDirTotalSize + endOfCentralDirectory.length;
    const zipFile = new Uint8Array(totalSize);
    
    // 重置偏移为0,开始写入文件条目
    currentOffset = 0;
    zipArray.forEach((entry) => {
        zipFile.set(entry, currentOffset);
        currentOffset += entry.length;
    });

    // 写入中央目录
    centralDirectoryArray.forEach((entry) => {
        zipFile.set(entry, currentOffset);
        currentOffset += entry.length;
    });

    // 写入EOCD
    zipFile.set(endOfCentralDirectory, currentOffset);

    const zipBlob = new Blob([zipFile], { type: 'application/zip' });
    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(zipBlob);
    link.download = zipFileName;
    link.click();
    return zipBlob;
}

// 辅助函数:将32位整数转换为小端字节数组
function uint32ToBytes(num) {
    return [
        num & 0xFF,
        (num >> 8) & 0xFF,
        (num >> 16) & 0xFF,
        (num >> 24) & 0xFF
    ];
}

// 辅助函数:将16位整数转换为小端字节数组
function uint16ToBytes(num) {
    return [
        num & 0xFF,
        (num >> 8) & 0xFF
    ];
}

const files = [
    {
        name: 'file1.txt',
        content: 'This is the content of file 1.',
    },
    {
        name: 'file2.txt',
        content: 'This is the content of file 2.',
    }
];

const zipFileName = 'Archive.zip';
zip(files, zipFileName);

关键修复点

  • 新增uint32ToBytes和uint16ToBytes辅助函数,确保所有多字节字段按Zip规范的小端格式写入。
  • 计算centralDirectory时,累加每个文件条目的完整长度(本地头+文件名+内容),记录正确的偏移值。
  • 生成zipFile前重置currentOffset为0,确保从文件起始位置开始写入数据。
  • 修正中央目录条目中的偏移字段引用,改为正确的file.offset并写入完整4字节。
  • 修复EOCD中的中央目录大小和偏移字段,使用完整4字节格式。

内容的提问来源于stack exchange,提问作者Hoppix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 00:59:51