mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(asar): let listing own parsed member facts
This commit is contained in:
1 parent
7cd65278ea
commit
85075fa979
3 files changed
+146
-56
No files matched your search
@@ -34,11 +34,7 @@ import {
|
||||
asarCancellationFailure,
|
||||
asarFailure,
|
||||
} from "./AsarFailure.js";
|
||||
import {
|
||||
readAsarListing,
|
||||
type AsarListedEntry,
|
||||
type AsarMetadata,
|
||||
} from "./AsarListing.js";
|
||||
import { readAsarListing, type AsarListedEntry } from "./AsarListing.js";
|
||||
|
||||
/**
|
||||
* Official Electron ASAR adapter with range-streamed member reads.
|
||||
@@ -90,39 +86,29 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
} catch (cause: unknown) {
|
||||
throw asarFailure(this.path, "inventory", cause);
|
||||
}
|
||||
for (const { metadata, path } of paths) {
|
||||
for (const facts of paths) {
|
||||
assertAsarReadActive(signal);
|
||||
const { path, kind } = facts;
|
||||
const providerPath = path.split("/").join(sep);
|
||||
const kind =
|
||||
metadata.files !== undefined
|
||||
? "directory"
|
||||
: metadata.link !== undefined
|
||||
? "symlink"
|
||||
: "file";
|
||||
const entry: ArtifactEntry = {
|
||||
path,
|
||||
kind,
|
||||
declaredSize: metadata.size ?? null,
|
||||
declaredSize: facts.size,
|
||||
compressedSize: null,
|
||||
executable: metadata.executable === true,
|
||||
executable: facts.executable,
|
||||
encrypted: false,
|
||||
byteOffset: null,
|
||||
declaredIntegrity:
|
||||
metadata.integrity !== undefined &&
|
||||
metadata.integrity.algorithm === "SHA256" &&
|
||||
/^[a-f0-9]{64}$/u.test(metadata.integrity.hash)
|
||||
? { algorithm: "sha256", digest: metadata.integrity.hash }
|
||||
: null,
|
||||
unpacked: "unpacked" in metadata && metadata.unpacked === true,
|
||||
declaredIntegrity: facts.declaredIntegrity,
|
||||
unpacked: facts.unpacked,
|
||||
limitations:
|
||||
kind === "symlink"
|
||||
? ["ASAR symlink target was not followed or disclosed."]
|
||||
: [],
|
||||
adapterKey: providerPath,
|
||||
};
|
||||
if (kind === "file" && isAsarFileMetadata(metadata))
|
||||
if (facts.kind === "file")
|
||||
this.#entries.set(providerPath, {
|
||||
metadata,
|
||||
facts,
|
||||
entry: copyArtifactEntry(entry),
|
||||
});
|
||||
yield entry;
|
||||
@@ -203,13 +189,10 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
state: AsarEntryState,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Readable> {
|
||||
const { metadata, entry: producedEntry } = state;
|
||||
const { facts, entry: producedEntry } = state;
|
||||
const archiveSize = this.#archiveSize;
|
||||
const headerSize = this.#headerSize;
|
||||
const offset =
|
||||
metadata.offset === undefined
|
||||
? undefined
|
||||
: parseArchiveOffset(metadata.offset);
|
||||
const offset = facts.packedOffset;
|
||||
const start =
|
||||
offset === undefined || headerSize === undefined
|
||||
? undefined
|
||||
@@ -218,10 +201,8 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
archiveSize === undefined ||
|
||||
start === undefined ||
|
||||
!Number.isSafeInteger(start) ||
|
||||
metadata.size < 0 ||
|
||||
!Number.isSafeInteger(metadata.size) ||
|
||||
start > archiveSize ||
|
||||
metadata.size > archiveSize - start
|
||||
facts.size > archiveSize - start
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
@@ -242,7 +223,7 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
"integrity",
|
||||
`ASAR container changed before read: ${producedEntry.path}`,
|
||||
);
|
||||
if (metadata.size === 0) {
|
||||
if (facts.size === 0) {
|
||||
handle = undefined;
|
||||
await closeAsarHandle(
|
||||
() => this.#closeOwnedHandle(openedHandle),
|
||||
@@ -252,13 +233,13 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
}
|
||||
const source = this.#ownedReadStream(openedHandle, {
|
||||
start,
|
||||
end: start + metadata.size - 1,
|
||||
end: start + facts.size - 1,
|
||||
signal,
|
||||
});
|
||||
handle = undefined;
|
||||
const output = readValidatedAsarEntry(
|
||||
source,
|
||||
metadata.size,
|
||||
facts.size,
|
||||
producedEntry.path,
|
||||
this.path,
|
||||
signal,
|
||||
@@ -609,9 +590,8 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
}
|
||||
}
|
||||
|
||||
type AsarFileMetadata = AsarMetadata & { readonly size: number };
|
||||
type AsarEntryState = {
|
||||
readonly metadata: AsarFileMetadata;
|
||||
readonly facts: Extract<AsarListedEntry, { readonly kind: "file" }>;
|
||||
readonly entry: ArtifactEntry;
|
||||
};
|
||||
type SharedAsarFileHandle = Pick<FileHandle, "fd" | "read" | "close"> & {
|
||||
@@ -654,19 +634,6 @@ const asarUnpackedEntryFailure = (
|
||||
return asarFailure(archivePath, `read ${entry.path}`, cause);
|
||||
};
|
||||
|
||||
const isAsarFileMetadata = (
|
||||
metadata: AsarMetadata,
|
||||
): metadata is AsarFileMetadata =>
|
||||
typeof metadata.size === "number" &&
|
||||
metadata.files === undefined &&
|
||||
metadata.link === undefined;
|
||||
|
||||
const parseArchiveOffset = (value: string): number | undefined => {
|
||||
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) return undefined;
|
||||
const offset = Number(value);
|
||||
return Number.isSafeInteger(offset) ? offset : undefined;
|
||||
};
|
||||
|
||||
const waitForOperation = async (
|
||||
operation: Promise<void>,
|
||||
signal?: AbortSignal,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { ArtifactReaderFailure, type ArtifactEntry } from "./ArtifactReader.js";
|
||||
import { assertAsarReadActive } from "./AsarFailure.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
@@ -23,11 +23,26 @@ const nodeSchema = z.looseObject({
|
||||
.looseObject({ algorithm: z.string(), hash: z.string() })
|
||||
.optional(),
|
||||
});
|
||||
export type AsarMetadata = z.infer<typeof nodeSchema>;
|
||||
export interface AsarListedEntry {
|
||||
readonly path: string;
|
||||
readonly metadata: AsarMetadata;
|
||||
}
|
||||
type AsarListedFacts = Pick<
|
||||
ArtifactEntry,
|
||||
"declaredIntegrity" | "executable" | "unpacked"
|
||||
> & { readonly path: string };
|
||||
export type AsarListedEntry = AsarListedFacts &
|
||||
(
|
||||
| { readonly kind: "directory" | "symlink"; readonly size: number | null }
|
||||
| {
|
||||
readonly kind: "file";
|
||||
readonly size: number;
|
||||
readonly packedOffset: number | undefined;
|
||||
}
|
||||
);
|
||||
|
||||
const parseArchiveOffset = (value: string | undefined): number | undefined => {
|
||||
if (value === undefined || !/^(?:0|[1-9][0-9]*)$/u.test(value))
|
||||
return undefined;
|
||||
const offset = Number(value);
|
||||
return Number.isSafeInteger(offset) ? offset : undefined;
|
||||
};
|
||||
|
||||
function* ownChildren(files: Record<string, unknown>) {
|
||||
for (const name in files)
|
||||
@@ -94,7 +109,29 @@ export const readAsarListing = async (
|
||||
"format",
|
||||
"ASAR entry has no directory, link or file metadata",
|
||||
);
|
||||
entries.push({ path, metadata });
|
||||
const facts: AsarListedFacts = {
|
||||
path,
|
||||
executable: metadata.executable === true,
|
||||
unpacked: metadata.unpacked === true,
|
||||
declaredIntegrity:
|
||||
metadata.integrity?.algorithm === "SHA256" &&
|
||||
/^[a-f0-9]{64}$/u.test(metadata.integrity.hash)
|
||||
? { algorithm: "sha256", digest: metadata.integrity.hash }
|
||||
: null,
|
||||
};
|
||||
if (metadata.files !== undefined || metadata.link !== undefined)
|
||||
entries.push({
|
||||
...facts,
|
||||
kind: metadata.files !== undefined ? "directory" : "symlink",
|
||||
size: metadata.size ?? null,
|
||||
});
|
||||
else if (metadata.size !== undefined)
|
||||
entries.push({
|
||||
...facts,
|
||||
kind: "file",
|
||||
size: metadata.size,
|
||||
packedOffset: parseArchiveOffset(metadata.offset),
|
||||
});
|
||||
if (metadata.files !== undefined)
|
||||
pending.push({
|
||||
parent: path,
|
||||
|
||||
@@ -139,3 +139,89 @@ it("preserves POSIX literal backslashes and rejects Windows component separators
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([undefined, "01", "-1", "9007199254740992"])(
|
||||
"retains inventory facts for an unreadable packed offset %s",
|
||||
async (offset) => {
|
||||
const digest = "a".repeat(64);
|
||||
const bytes = asarHeaderBytes(
|
||||
JSON.stringify({
|
||||
files: {
|
||||
member: {
|
||||
size: 0,
|
||||
...(offset === undefined ? {} : { offset }),
|
||||
executable: true,
|
||||
integrity: { algorithm: "SHA256", hash: digest },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const path = await archiveWith(bytes);
|
||||
const reader = new AsarArtifactReader(path);
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
expect(entries).toHaveLength(1);
|
||||
const entry = entries[0]!;
|
||||
expect(entry).toMatchObject({
|
||||
path: "member",
|
||||
kind: "file",
|
||||
declaredSize: 0,
|
||||
executable: true,
|
||||
declaredIntegrity: { algorithm: "sha256", digest },
|
||||
});
|
||||
await expect(reader.open(entry)).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
});
|
||||
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves directory and link priority with unrecognized integrity declarations", async () => {
|
||||
const bytes = asarHeaderBytes(
|
||||
JSON.stringify({
|
||||
files: {
|
||||
directory: {
|
||||
files: {},
|
||||
link: "target",
|
||||
size: 3,
|
||||
integrity: { algorithm: "SHA512", hash: "source-digest" },
|
||||
},
|
||||
link: {
|
||||
link: "target",
|
||||
size: 4,
|
||||
offset: "invalid",
|
||||
integrity: { algorithm: "SHA256", hash: "INVALID" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const path = await archiveWith(bytes);
|
||||
const reader = new AsarArtifactReader(path);
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
expect(entries).toMatchObject([
|
||||
{
|
||||
path: "directory",
|
||||
kind: "directory",
|
||||
declaredSize: 3,
|
||||
declaredIntegrity: null,
|
||||
limitations: [],
|
||||
},
|
||||
{
|
||||
path: "link",
|
||||
kind: "symlink",
|
||||
declaredSize: 4,
|
||||
declaredIntegrity: null,
|
||||
limitations: ["ASAR symlink target was not followed or disclosed."],
|
||||
},
|
||||
]);
|
||||
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user