Skip to content

Instantly share code, notes, and snippets.

@PhoenixIllusion
Last active December 17, 2025 17:19
Show Gist options
  • Select an option

  • Save PhoenixIllusion/5e5cad5d469f64d33b676fd92ca0e0c4 to your computer and use it in GitHub Desktop.

Select an option

Save PhoenixIllusion/5e5cad5d469f64d33b676fd92ca0e0c4 to your computer and use it in GitHub Desktop.
Minimal ZipReader that uses js decompression stream and supports Zip64 Central Directory
interface ZipEntry<T> {
name: string;
path: string;
entry?: T;
children?: ZipEntry<T>[]
}
function splitPath(path: string) {
if(path.indexOf('/')<0) {
return { parent: '', name: path}
}
const parent = path.substring(0, path.lastIndexOf('/'));
const name = path.substring(path.lastIndexOf('/')+1);
return { parent, name };
}
export function parseFolders<T>(records: T[], getFilename: (r: T)=>string): ZipEntry<T> {
const exists = new Map<string, ZipEntry<T>>();
const root = { name: '', path: '/', children: []};
exists.set('', root);
function createDirIfNeeded(path: string): ZipEntry<T> {
const dir = exists.get(path);
if(!dir) {
const { parent, name } = splitPath(path);
const parentEntry = createDirIfNeeded(parent);
const result: ZipEntry<T> = { name, path, children: []};
exists.set(path, result);
parentEntry.children?.push(result);
return result;
}
return dir;
}
records.forEach(record => {
const filename = getFilename(record);
if(!filename.endsWith('/')) {
const { parent, name } = splitPath(filename);
const folder = createDirIfNeeded(parent);
folder.children?.push({ name, path: filename, entry: record })
}
});
return root;
}
import { parseFolders } from './folder';
import './style.css'
import { ZipReader } from './MinZipReader';
const zipFile = document.getElementById('zipFile') as HTMLInputElement;
zipFile.addEventListener('change', async () => {
const files = zipFile.files;
if(files && files[0]) {
const zip = new ZipReader(files[0]);
await zip.parse();
const folders = parseFolders(zip.r, (R) => R[5]);
const innerZipBlob = await zip.getFile(zip.r.find(x => x[5] == "Archive/Isometric Road Assets/Isometric Road Assets.zip")!);
const innerZip = new ZipReader(innerZipBlob);
await innerZip.parse();
const innerZipFolders = parseFolders(innerZip.r, (R) => R[5]);
debugger;
}
})
export class ZipReader {
/** [Method, CRC, CSize, USize, Offset, Name] */
r:any[][]=[];
b: Blob;
constructor(b:Blob){this.b=b;}
async parse(){
const V=(b:Blob)=>b.arrayBuffer().then(a=>new DataView(a)),
g=(v:DataView,i: number,b: number)=>Number((v as any)[(b>32?'getBig':'get')+'Uint'+b](i,1)),
td=new TextDecoder();
let t=this.b,d=await V(t.slice(-65536)),l=65532;
while(l--&&g(d,l,32)!=0x6054b50);
if(l<0)throw 0;
let n=g(d,l+10,16),s=g(d,l+12,32),o=g(d,l+16,32);
if(l>20&&g(d,l-20,32)==0x7064b50){
d=await V(t.slice(g(d,l-12,64)));
n=g(d,32,64);s=g(d,40,64);o=g(d,48,64);
}
d=await V(t.slice(o,o+s));
for(let i=0;n--;){
if(g(d,i,32)!=0x2014b50)throw 1;
let nl=g(d,i+28,16),el=g(d,i+30,16),cl=g(d,i+32,16);
this.r.push([
g(d,i+10,16), g(d,i+16,32),g(d,i+20,32),g(d,i+24,32),g(d,i+42,32),
td.decode(new Uint8Array(d.buffer,d.byteOffset+i+46,nl))
]);
i+=46+nl+el+cl;
}
}
async getFile(x:number[]){
let b=this.b,h=new DataView(await b.slice(x[4],x[4]+30).arrayBuffer()),
st=x[4]+30+h.getUint16(26,true)+h.getUint16(28,true),
c=b.slice(st,st+x[2]);
if(!x[0])return c;
return new Response(new Blob([
new Uint8Array([31,139,8,0,0,0,0,0,0,255]), c, new Uint32Array([x[1],x[3]])
]).stream().pipeThrough(new DecompressionStream('gzip'))).blob();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment