sc-compression
Version:
Compress and decompress Supercell games assets
110 lines (104 loc) • 3.59 kB
JavaScript
/* eslint-disable no-param-reassign */
import { Buffer } from 'node:buffer';
import * as lzham from './lzham.mjs';
import * as lzma from './lzma.mjs';
import zstd from './zstd.mjs';
const crypto = await import('node:crypto');
const readSignature = (buffer) => {
if (Buffer.compare(buffer.slice(0, 3), Buffer.from('5d0000', 'hex')) === 0) {
return 'lzma';
}
if (buffer.slice(0, 2).toString('utf8').toLowerCase() === 'sc') {
if (
buffer.length >= 30 &&
buffer.slice(26, 30).toString('utf8').toLowerCase() === 'sclz'
) {
return 'sclz';
}
if (
buffer.length >= 34 &&
buffer.slice(30, 34).toString('hex') === '28b52ffd'
) {
return 'zstd';
}
if (buffer.indexOf('START', 'utf8') > -1) {
return 'sc2';
}
return 'sc';
}
if (buffer.slice(0, 4).toString('utf8').toLowerCase() === 'sig:') {
return 'sig';
}
return 'none';
};
const decompress = async (buffer) => {
const signature = readSignature(buffer);
let decompressed = buffer;
let rest;
if (signature === 'sc') {
buffer = buffer.slice(26);
} else if (signature === 'sc2' || signature === 'zstd') {
buffer = buffer.slice(30);
const endOffset = buffer.indexOf('START', 'utf8');
buffer = buffer.slice(0, endOffset);
rest = buffer.slice(endOffset + 5);
} else if (signature === 'sig') {
buffer = buffer.slice(68);
} else if (signature === 'sclz') {
const dictSizeLog2 = buffer.readUInt8(30);
const outputSize = buffer.readInt32LE(31);
decompressed = await lzham.decompress(buffer.slice(35), {
dictSizeLog2,
outputSize,
});
}
if (['lzma', 'sc', 'sc2', 'sig'].includes(signature)) {
const uncompressedSize = buffer.readInt32LE(5);
const padded = Buffer.concat([
buffer.slice(0, 9),
Buffer.allocUnsafe(4).fill(uncompressedSize === -1 ? 0xff : 0),
buffer.slice(9),
]);
decompressed = await lzma.decompress(padded);
}
if (signature === 'zstd') {
decompressed = await zstd.decompress(buffer);
}
if (rest != null) {
decompressed = Buffer.concat([decompressed, rest]);
}
return decompressed;
};
const compress = async (buffer, signature = 'sc') => {
if (signature === 'none') return buffer;
if (signature === 'lzma' || signature === 'sc' || signature === 'sig') {
const compressed = await lzma.compress(buffer, { preset: 6 });
buffer = Buffer.concat([compressed.slice(0, 9), compressed.slice(13)]);
} else if (signature === 'sclz') {
const lzhamHeader = Buffer.allocUnsafe(9);
lzhamHeader.write('SCLZ', 0, 4, 'ascii');
lzhamHeader.writeUInt8(18, 4);
lzhamHeader.writeInt32LE(buffer.length, 5);
buffer = Buffer.concat([lzhamHeader, lzham.compress(buffer)]);
} else {
throw new Error(`invalid signature ${signature}`);
}
if (signature === 'sc' || signature === 'sclz') {
const scHeader = Buffer.allocUnsafe(10);
scHeader.write('SC', 0, 2, 'ascii');
scHeader.writeInt32BE(1, 2);
scHeader.writeInt32BE(16, 6);
const bufferHash = crypto.createHash('md5').update(buffer).digest('hex');
buffer = Buffer.concat([scHeader, Buffer.from(bufferHash, 'hex'), buffer]);
} else if (signature === 'sig') {
const sigHeader = Buffer.from('5369673a', 'hex');
const sha64 = Buffer.alloc(64);
buffer = Buffer.concat([sigHeader, sha64, buffer]);
// eslint-disable-next-line no-console
console.warn(
'compressed sig files cannot be signed, which will result in client crashes'
);
}
return buffer;
};
export { compress, decompress, readSignature };