Skip to content

Instantly share code, notes, and snippets.

@eai04191
Created June 14, 2024 10:20
Show Gist options
  • Save eai04191/c532a47caad96bc348fee9fd08aab2a7 to your computer and use it in GitHub Desktop.
Save eai04191/c532a47caad96bc348fee9fd08aab2a7 to your computer and use it in GitHub Desktop.
export interface ZipInfo {
compressedSize: number;
fileNameLength: number;
extraFieldLength: number;
fileName: string;
files: ZipFile[];
}
export interface ZipFile {
filename: string;
data: Blob;
}
export async function parseZipFromStream(
stream: ReadableStream<Uint8Array>,
): Promise<ZipInfo> {
const reader = stream.getReader();
let buffer = new Uint8Array(0);
// データを必要な長さまで読み込むヘルパー関数
const readIntoBuffer = async (size: number): Promise<void> => {
while (buffer.length < size) {
const { done, value } = await reader.read();
if (done) throw new Error("Unexpected end of stream");
buffer = concatUint8Arrays(buffer, value);
}
};
// データを指定の位置から読む関数
const readUint16 = (offset: number): number => {
return buffer[offset] | (buffer[offset + 1] << 8);
};
const readUint32 = (offset: number): number => {
return (
buffer[offset] |
(buffer[offset + 1] << 8) |
(buffer[offset + 2] << 16) |
(buffer[offset + 3] << 24)
);
};
const readString = (offset: number, length: number): string => {
return new TextDecoder().decode(
buffer.subarray(offset, offset + length),
);
};
const zipFile: ZipInfo = {
compressedSize: 0,
fileNameLength: 0,
extraFieldLength: 0,
fileName: "",
files: [],
};
while (true) {
await readIntoBuffer(30); // 最小ヘッダーサイズ
if (readUint32(0) !== 0x04034b50) break; // ローカルファイルヘッダーのシグネチャ
const compressedSize = readUint32(18);
const fileNameLength = readUint16(26);
const extraFieldLength = readUint16(28);
await readIntoBuffer(
30 + fileNameLength + extraFieldLength + compressedSize,
);
const fileName = readString(30, fileNameLength);
const headerOffset = 30 + fileNameLength + extraFieldLength;
const fileData = buffer.subarray(
headerOffset,
headerOffset + compressedSize,
);
buffer = buffer.subarray(headerOffset + compressedSize); // バッファをスライスして次のエントリに対応
const decompressedData = await decompressData(fileData);
const fileEntry: ZipFile = {
filename: fileName,
data: decompressedData,
};
zipFile.files.push(fileEntry);
zipFile.compressedSize += compressedSize;
zipFile.fileNameLength += fileNameLength;
zipFile.extraFieldLength += extraFieldLength;
}
return zipFile;
}
async function decompressData(data: Uint8Array): Promise<Blob> {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(data);
controller.close();
},
});
const decompressionStream = new DecompressionStream("deflate-raw");
const decompressedStream = stream.pipeThrough(decompressionStream);
const reader = decompressedStream.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) chunks.push(value);
}
const decompressedData = concatUint8Arrays(...chunks);
return new Blob([decompressedData]);
}
function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment