文件压缩时出现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
问题分析
currentOffset未重置:生成zipFile时使用的currentOffset是之前计算文件内容总长度的变量,初始值不为0,导致写入第一个文件条目时就从错误偏移开始,后续写入中央目录时偏移超出zipFile长度范围。- 中央目录偏移计算错误:最初计算
centralDirectory中的offset时,只累加了文件内容长度,未包含本地文件头和文件名的长度,不符合Zip格式规范;同时中央目录条目中引用偏移时写错字段(file.currentOffset应为file.offset),且偏移仅写入2字节(Zip规范要求4字节)。 - 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
相关产品推荐
相关产品推荐

