infrastructure for inline vfs and clients

This commit is contained in:
tresabhi committed 2026-07-31 20:48:33 -05:00
1 parent d81d885767
commit 39745dd475
19 files changed
+133 -64

No files matched your search

-79
View File
@@ -1,79 +0,0 @@
import { readDVPL } from "@blitzkit/core";
import { Options, parse as parseCSV } from "csv-parse/sync";
import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix";
import { parse as parseYaml } from "yaml";
export abstract class AbstractVFS {
textDecoder = new TextDecoder();
private xmlParser = new XMLParser();
abstract init(): Promise<typeof this>;
abstract dispose(): void;
abstract has(path: string): Promise<boolean>;
abstract raw(path: string): Promise<Uint8Array>;
async resolve(path: string) {
const normalized = normalize(path);
if (await this.has(normalized)) return normalized;
const dvplPath = `${normalized}.dvpl`;
if (await this.has(dvplPath)) return dvplPath;
return null;
}
async assert(path: string) {
const resolved = await this.resolve(path);
if (resolved === null) throw new Error(`File not found: ${path}`);
return resolved;
}
async file(file: string) {
const resolved = await this.assert(file);
const raw = await this.raw(resolved);
let buffer = raw;
if (resolved.endsWith(".dvpl")) {
buffer = new Uint8Array(readDVPL(Buffer.from(raw)));
}
return buffer;
}
abstract dir(path: string): Promise<string[]>;
async text(path: string) {
const file = await this.file(path);
return this.textDecoder.decode(file);
}
async yaml<Type>(path: string) {
const file = await this.text(path);
return parseYaml(file) as Type;
}
async xml<Type>(path: string) {
const file = await this.text(path);
return this.xmlParser.parse(file) as Type;
}
async _csv(text: string, options: Options = {}) {
const parsed = parseCSV(text, options);
return parsed;
}
async csv(path: string, options: Options = {}) {
const text = await this.text(path);
return await this._csv(text, options);
}
[Symbol.dispose]() {
this.dispose();
}
}
-35
View File
@@ -1,35 +0,0 @@
import { readdir } from "node:fs/promises";
import { AbstractVFS } from "./abstract";
export class LocalVFS extends AbstractVFS {
constructor(private base: string) {
super();
}
async init() {
return this;
}
async has(path: string) {
const file = Bun.file(`${this.base}/${path}`);
return await file.exists();
}
async raw(path: string) {
const file = Bun.file(`${this.base}/${path}`);
const buffer = await file.arrayBuffer();
const array = new Uint8Array(buffer);
return array;
}
async dir(path: string) {
try {
return await readdir(`${this.base}/${path}`);
} catch (_) {
return [];
}
}
dispose() {}
}
-49
View File
@@ -1,49 +0,0 @@
import { AbstractVFS } from "./abstract";
export class MixedVFS extends AbstractVFS {
constructor(private vfses: AbstractVFS[]) {
super();
}
async init() {
for (const vfs of this.vfses) {
await vfs.init();
}
return this;
}
dispose() {
for (const vfs of this.vfses) {
vfs.dispose();
}
}
async has(path: string) {
for (const vfs of this.vfses) {
if (await vfs.has(path)) return true;
}
return false;
}
async raw(path: string) {
for (const vfs of this.vfses) {
if (await vfs.has(path)) return await vfs.raw(path);
}
throw new Error(`File not found: ${path}`);
}
async dir(path: string) {
const files = new Set<string>();
for (const vfs of this.vfses) {
for (const file of await vfs.dir(path)) {
files.add(file);
}
}
return Array.from(files);
}
}
-111
View File
@@ -1,111 +0,0 @@
import SteamUser, { EConnectionProtocol } from "steam-user";
import { AbstractVFS } from "./abstract";
interface SteamManifestFile {
chunks: {
sha: string;
crc: number;
offset: string;
cb_original: number;
cb_compressed: number;
}[];
filename: string;
size: string;
flags: number;
sha_filename: string;
sha_content: string;
linktarget: unknown;
}
interface SteamManifest {
files: SteamManifestFile[];
depot_id: number;
gid_manifest: string;
creation_time: number;
filenames_encrypted: boolean;
cb_disk_original: string;
cb_disk_compressed: string;
unique_chunks: number;
crc_encrypted: number;
crc_clear: number;
}
export class SteamVFS extends AbstractVFS {
private steam = new SteamUser({ protocol: EConnectionProtocol.TCP });
private manifest: Map<string, SteamManifestFile> = new Map();
constructor(
private username: string,
private password: string,
private app: number,
private depot: number,
) {
super();
}
async init() {
this.steam.logOn({
accountName: this.username,
password: this.password,
});
await new Promise((resolve) => this.steam.once("loggedOn", resolve));
const productInfo = await this.steam.getProductInfo([this.app], []);
const manifest: SteamManifest = await new Promise((resolve) => {
// @ts-expect-error
this.steam.getManifest(
this.app,
this.depot,
// @ts-expect-error
productInfo.apps[this.app].appinfo.depots[this.depot].manifests.public
.gid,
"public",
// @ts-expect-error
(_, response) => {
resolve(response);
},
);
});
for (const file of manifest.files) {
const path = file.filename.replaceAll("\\", "/");
this.manifest.set(path, file);
}
return this;
}
async has(path: string) {
return this.manifest.has(path);
}
async raw(path: string) {
const fileManifest = this.manifest.get(path)!;
const downloaded: { type: "complete"; file: Buffer } =
// @ts-expect-error
await this.steam.downloadFile(this.app, this.depot, fileManifest);
return new Uint8Array(downloaded.file);
}
async dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
for (const child of this.manifest.keys()) {
if (!child.startsWith(path) || child === path) continue;
const childSegments = child.split("/");
const nextSegment = childSegments[parentSegments];
children.add(nextSegment);
}
return Array.from(children);
}
dispose() {
this.steam.logOff();
}
}
-53
View File
@@ -1,53 +0,0 @@
import { normalize } from "path/posix";
import { HTTPRangeReader, unzip, ZipEntry } from "unzipit";
import { AbstractVFS } from "./abstract";
export class ZipVFS extends AbstractVFS {
entries = new Map<string, ZipEntry>();
constructor(
private url: string,
private root = "app/",
) {
super();
}
async init() {
const reader = new HTTPRangeReader(this.url);
const { entries } = await unzip(reader);
for (const entry of Object.values(entries)) {
const normalized = normalize(entry.name.slice(this.root.length));
this.entries.set(normalized, entry);
}
return this;
}
async has(path: string) {
return this.entries.has(path);
}
async raw(path: string) {
const arrayBuffer = await this.entries.get(path)!.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
async dir(path: string) {
const parentSegments = path.split("/").length;
const children = new Set<string>();
for (const child of this.entries.keys()) {
if (!child.startsWith(path) || child === path) continue;
const childSegments = child.split("/");
const nextSegment = childSegments[parentSegments];
children.add(nextSegment);
}
return Array.from(children);
}
dispose() {}
}