import { createFromBuffer } from "chromium-pickle-js"
import { close, open, read, readFile, Stats } from "fs-extra"
import * as path from "path"
export interface ReadAsarHeader {
readonly header: string
readonly size: number
}
export interface NodeIntegrity {
algorithm: "SHA256"
hash: string
blockSize: number
blocks: Array<string>
}
export class Node {
files?: Record<string, Node>
unpacked?: boolean
size?: number
offset?: string
executable?: boolean
link?: string
integrity?: NodeIntegrity
}
export class AsarFilesystem {
private offset = 0
constructor(
readonly src: string,
readonly header = new Node(),
readonly headerSize: number = -1
) {
if (this.header.files == null) {
this.header.files = this.newNode()
}
}
searchNodeFromDirectory(p: string, isCreate: boolean): Node | null {
let node = this.header
for (const dir of p.split(path.sep)) {
if (dir !== ".") {
let child = node.files![dir]
if (child == null) {
if (!isCreate) {
return null
}
child = new Node()
child.files = this.newNode()
node.files![dir] = child
}
node = child
}
}
return node
}
getOrCreateNode(p: string): Node {
if (p == null || p.length === 0) {
return this.header
}
const name = path.basename(p)
const dirNode = this.searchNodeFromDirectory(path.dirname(p), true)!
if (dirNode.files == null) {
dirNode.files = this.newNode()
}
let result = dirNode.files[name]
if (result == null) {
result = new Node()
dirNode.files[name] = result
}
return result
}
addFileNode(file: string, dirNode: Node, size: number, unpacked: boolean, stat: Stats, integrity?: NodeIntegrity): Node {
if (size > 4294967295) {
throw new Error(`${file}: file size cannot be larger than 4.2GB`)
}
const node = new Node()
node.size = size
if (integrity) {
node.integrity = integrity
}
if (unpacked) {
node.unpacked = true
} else {
node.offset = this.offset.toString()
if (process.platform !== "win32" && stat.mode & 0o100) {
node.executable = true
}
this.offset += node.size
}
let children = dirNode.files
if (children == null) {
children = this.newNode()
dirNode.files = children
}
children[path.basename(file)] = node
return node
}
getNode(p: string): Node | null {
const node = this.searchNodeFromDirectory(path.dirname(p), false)!
return node.files![path.basename(p)]
}
getFile(p: string, followLinks = true): Node {
const info = this.getNode(p)!
return followLinks && info.link != null ? this.getFile(info.link) : info
}
async readJson(file: string): Promise<any> {
return JSON.parse((await this.readFile(file)).toString())
}
readFile(file: string): Promise<Buffer> {
return readFileFromAsar(this, file, this.getFile(file))
}
private newNode() {
return Object.create(null) as Record<string, Node>
}
}
export async function readAsarHeader(archive: string): Promise<ReadAsarHeader> {
const fd = await open(archive, "r")
let size: number
let headerBuf
try {
const sizeBuf = Buffer.allocUnsafe(8)
if ((await read(fd, sizeBuf, 0, 8, null as any)).bytesRead !== 8) {
throw new Error("Unable to read header size")
}
const sizePickle = createFromBuffer(sizeBuf)
size = sizePickle.createIterator().readUInt32()
headerBuf = Buffer.allocUnsafe(size)
if ((await read(fd, headerBuf, 0, size, null as any)).bytesRead !== size) {
throw new Error("Unable to read header")
}
} finally {
await close(fd)
}
const headerPickle = createFromBuffer(headerBuf)
return { header: headerPickle.createIterator().readString(), size }
}
export async function readAsar(archive: string): Promise<AsarFilesystem> {
const { header, size } = await readAsarHeader(archive)
return new AsarFilesystem(archive, JSON.parse(header), size)
}
export async function readAsarJson(archive: string, file: string): Promise<any> {
const fs = await readAsar(archive)
return await fs.readJson(file)
}
async function readFileFromAsar(filesystem: AsarFilesystem, filename: string, info: Node): Promise<Buffer> {
const size = info.size!
const buffer = Buffer.allocUnsafe(size)
if (size <= 0) {
return buffer
}
if (info.unpacked) {
return await readFile(path.join(`${filesystem.src}.unpacked`, filename))
}
const fd = await open(filesystem.src, "r")
try {
const offset = 8 + filesystem.headerSize + parseInt(info.offset!, 10)
await read(fd, buffer, 0, size, offset)
} finally {
await close(fd)
}
return buffer
}