Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@
"require": "./dist/cia.js",
"types": "./dist/cia.d.ts"
},
"./compression/cmp": {
"import": "./dist/compression/cmp.js",
"require": "./dist/compression/cmp.js",
"types": "./dist/compression/cmp.d.ts"
},
"./compression/yaz0": {
"import": "./dist/compression/yaz0.js",
"require": "./dist/compression/yaz0.js",
"types": "./dist/compression/yaz0.d.ts"
},
"./msbt": {
"import": "./dist/msbt.js",
"require": "./dist/msbt.js",
Expand Down
114 changes: 114 additions & 0 deletions src/compression/cmp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import zlib from 'node:zlib';
import FileStream from '@/file-stream';

// * Unsure what the real name of this is. Switch Toolbox uses the ZCMP class for
// * files that end with `*.cmp`, however `main.sgarc.cmp` found in Mii Maker has
// * a different format? This is designed for the file(s) in Mii Maker

/**
* CMP handles the decompression of files using ZLIB. Files of this type usually end
* with `*.cmp`
*/
export default class CMP {
private stream: FileStream;

/**
* Decompresses the CMP-compressed data from the provided `fdOrPath`
*
* @param fdOrPath - Either an open `fd` or a path to a file on disk
*/
public decompressFromFile(fdOrPath: number | string): Buffer {
this.stream = new FileStream(fdOrPath);
return this.decompress();
}

/**
* Decompresses the CMP-compressed data from the provided `buffer`
*
* @param buffer - CMP-compressed data buffer
*/
public decompressFromBuffer(buffer: Buffer): Buffer {
this.stream = new FileStream(buffer);
return this.decompress();
}

/**
* Decompresses the CMP-compressed data from the provided string
*
* Calls `decompressFromBuffer` internally
*
* @param base64 - Base64 encoded CMP-compressed data
*/
public decompressFromString(base64: string): Buffer {
return this.decompressFromBuffer(Buffer.from(base64, 'base64'));
}

/**
* Decompresses the CMP-compressed data from an existing file stream
*
* @param stream - An existing file stream
*/
public decompressFromFileStream(stream: FileStream): Buffer {
this.stream = stream;
return this.decompress();
}

/**
* Creates a new instance of `CMP` and
* parses the CMP-compressed data from the provided `fdOrPath`
*
* @param fdOrPath - Either an open `fd` or a path to a file on disk
*/
public static fromFile(fdOrPath: number | string): Buffer {
const cmp = new CMP();
return cmp.decompressFromFile(fdOrPath);
}

/**
* Creates a new instance of `CMP` and
* parses the CMP-compressed data from the provided `buffer`
*
* @param buffer - CMP-compressed data buffer
*/
public static fromBuffer(buffer: Buffer): Buffer {
const cmp = new CMP();
return cmp.decompressFromBuffer(buffer);
}

/**
* Creates a new instance of `CMP` and
* parses the CMP-compressed data from the provided string
*
* Calls `decompressFromBuffer` internally
*
* @param base64 - Base64 encoded CMP-compressed data
*/
public static fromString(base64: string): Buffer {
const cmp = new CMP();
return cmp.decompressFromString(base64);
}

/**
* Creates a new instance of `CMP` and
* parses the CMP-compressed data from an existing file stream
*
* @param stream - An existing file stream
*/
public static fromFileStream(stream: FileStream): Buffer {
const cmp = new CMP();
return cmp.decompressFromFileStream(stream);
}

private decompress(): Buffer {
const decompressedSize = this.stream.readUInt32BE();
const compressedSize = this.stream.remaining();
const compressed = this.stream.readBytes(compressedSize);
const decompressed = zlib.inflateSync(compressed);

if (decompressed.length !== decompressedSize) {
throw new Error(`Invalid decompressed size. Expected ${decompressedSize}, got ${decompressed.length}`);
}

return decompressed;
}
}
5 changes: 5 additions & 0 deletions src/compression/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as CMP } from '@/compression/cmp';
export * from '@/compression/cmp';

export { default as Yaz0 } from '@/compression/yaz0';
export * from '@/compression/yaz0';
153 changes: 153 additions & 0 deletions src/compression/yaz0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import FileStream from '@/file-stream';
import Stream from '@/stream';

const YAZ0_MAGIC = Buffer.from('Yaz0');

/**
* Yaz0 handles the decompression of files using Yaz0
*/
export default class Yaz0 {
private stream: FileStream;

/**
* Decompresses the Yaz0-compressed data from the provided `fdOrPath`
*
* @param fdOrPath - Either an open `fd` or a path to a file on disk
*/
public decompressFromFile(fdOrPath: number | string): Buffer {
this.stream = new FileStream(fdOrPath);
return this.decompress();
}

/**
* Decompresses the Yaz0-compressed data from the provided `buffer`
*
* @param buffer - Yaz0-compressed data buffer
*/
public decompressFromBuffer(buffer: Buffer): Buffer {
this.stream = new FileStream(buffer);
return this.decompress();
}

/**
* Decompresses the Yaz0-compressed data from the provided string
*
* Calls `decompressFromBuffer` internally
*
* @param base64 - Base64 encoded Yaz0-compressed data
*/
public decompressFromString(base64: string): Buffer {
return this.decompressFromBuffer(Buffer.from(base64, 'base64'));
}

/**
* Decompresses the Yaz0-compressed data from an existing file stream
*
* @param stream - An existing file stream
*/
public decompressFromFileStream(stream: FileStream): Buffer {
this.stream = stream;
return this.decompress();
}

/**
* Creates a new instance of `Yaz0` and
* parses the Yaz0-compressed data from the provided `fdOrPath`
*
* @param fdOrPath - Either an open `fd` or a path to a file on disk
*/
public static fromFile(fdOrPath: number | string): Buffer {
const yaz0 = new Yaz0();
return yaz0.decompressFromFile(fdOrPath);
}

/**
* Creates a new instance of `Yaz0` and
* parses the Yaz0-compressed data from the provided `buffer`
*
* @param buffer - Yaz0-compressed data buffer
*/
public static fromBuffer(buffer: Buffer): Buffer {
const yaz0 = new Yaz0();
return yaz0.decompressFromBuffer(buffer);
}

/**
* Creates a new instance of `Yaz0` and
* parses the Yaz0-compressed data from the provided string
*
* Calls `decompressFromBuffer` internally
*
* @param base64 - Base64 encoded Yaz0-compressed data
*/
public static fromString(base64: string): Buffer {
const yaz0 = new Yaz0();
return yaz0.decompressFromString(base64);
}

/**
* Creates a new instance of `Yaz0` and
* parses the Yaz0-compressed data from an existing file stream
*
* @param stream - An existing file stream
*/
public static fromFileStream(stream: FileStream): Buffer {
const yaz0 = new Yaz0();
return yaz0.decompressFromFileStream(stream);
}

private decompress(): Buffer {
const magic = this.stream.readBytes(4);

if (!YAZ0_MAGIC.equals(magic)) {
throw new Error('Invalid Yaz0 magic');
}

const decompressedSize = this.stream.readUInt32BE();

this.stream.skip(4); // * Reserved, ignore for now
this.stream.skip(4); // * Reserved, ignore for now

const compressedSize = this.stream.remaining();
const compressed = this.stream.readBytes(compressedSize);
const compressedStream = new Stream(compressed);
const decompressed = Buffer.alloc(decompressedSize);
let outputBytePosition = 0;

while (compressedStream.remaining()) {
const groupHeader = compressedStream.readUInt8();

for (let chunk = 0; chunk < 8 && compressedStream.remaining(); chunk++) {
const bitMask = 0x80 >> chunk;
const isLiteral = (groupHeader & bitMask) !== 0;
const firstByte = compressedStream.readUInt8();

if (isLiteral) {
decompressed[outputBytePosition++] = firstByte;
} else {
const secondByte = compressedStream.readUInt8();

const distance = ((firstByte & 0x0F) << 8) | secondByte;
let length: number;

if ((firstByte & 0xF0) !== 0) {
const n = (firstByte & 0xF0) >> 4;
length = n + 2;
} else {
const thirdByte = compressedStream.readUInt8();
length = thirdByte + 0x12;
}

const backReferenceStart = outputBytePosition - distance - 1;

for (let i = 0; i < length && outputBytePosition < decompressedSize; i++) {
decompressed[outputBytePosition] = decompressed[backReferenceStart + i];
outputBytePosition++;
}
}
}
}

return decompressed;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export * from '@/certificate';
export { default as CIA } from '@/cia';
export * from '@/cia';

export * from '@/compression';

export { default as MSBT } from '@/msbt';
export * from '@/msbt';

Expand Down