Commit b00b1f56 authored by nanahira's avatar nanahira

add sha512 for resource

parent 82eb4be8
Pipeline #43367 passed with stages
in 3 minutes and 15 seconds
import * as fs from 'node:fs';
import { createHash, Hash } from 'node:crypto';
import { searchYGOProResource } from 'koishipro-core.js';
import type { CardDataEntry } from 'ygopro-cdb-encode';
import { YGOProCdb } from 'ygopro-cdb-encode';
......@@ -12,6 +13,17 @@ const isFileNotFoundError = (error: unknown): error is NodeJS.ErrnoException =>
'code' in error &&
(error as NodeJS.ErrnoException).code === 'ENOENT';
const hashWithSizePrefix = (hash: Hash, payload: Buffer) => {
const sizePrefix = Buffer.allocUnsafe(4);
sizePrefix.writeUInt32BE(payload.length >>> 0, 0);
hash.update(sizePrefix);
hash.update(payload);
};
const hashWithSizePrefixText = (hash: Hash, text: string) => {
hashWithSizePrefix(hash, Buffer.from(text, 'utf8'));
};
export class CardLoadWorkerResult {
@TransportType(() => CardStorage)
cardStorage: CardStorage;
......@@ -20,7 +32,7 @@ export class CardLoadWorkerResult {
failedFiles: string[];
@TransportType(() => Buffer)
ocgcoreWasmBinary?: Buffer;
sha512: Buffer;
}
@DefineWorker()
......@@ -38,6 +50,7 @@ export class CardLoadWorker {
const seen = new Set<number>();
let dbCount = 0;
const failedFiles: string[] = [];
const sha512 = createHash('sha512');
for await (const file of searchYGOProResource(...this.ygoproPaths)) {
if (!file.path.endsWith('.cdb')) {
......@@ -45,7 +58,8 @@ export class CardLoadWorker {
}
try {
const currentDb = new SQL.Database(await file.read());
const cdbBody = await file.read();
const currentDb = new SQL.Database(cdbBody);
try {
const currentCdb = new YGOProCdb(currentDb).noTexts();
for (const card of currentCdb.step()) {
......@@ -57,6 +71,8 @@ export class CardLoadWorker {
cards.push(card);
}
++dbCount;
hashWithSizePrefixText(sha512, file.path);
hashWithSizePrefix(sha512, Buffer.from(cdbBody));
} finally {
currentDb.close();
}
......@@ -69,7 +85,11 @@ export class CardLoadWorker {
let ocgcoreWasmBinary: Buffer | undefined;
if (this.ocgcoreWasmPath) {
try {
const wasmBinary = await fs.promises.readFile(this.ocgcoreWasmPath);
const wasmBinary = Buffer.from(
await fs.promises.readFile(this.ocgcoreWasmPath),
);
hashWithSizePrefixText(sha512, this.ocgcoreWasmPath);
hashWithSizePrefix(sha512, wasmBinary);
ocgcoreWasmBinary = toShared(wasmBinary);
} catch (error) {
if (!isFileNotFoundError(error)) {
......@@ -79,10 +99,12 @@ export class CardLoadWorker {
}
const result = new CardLoadWorkerResult();
result.cardStorage = toShared(CardStorage.fromCards(cards));
result.cardStorage = toShared(
CardStorage.fromCards(cards, ocgcoreWasmBinary),
);
result.dbCount = dbCount;
result.failedFiles = failedFiles;
result.ocgcoreWasmBinary = ocgcoreWasmBinary;
result.sha512 = toShared(sha512.digest());
return result;
}
}
......@@ -25,6 +25,9 @@ export class CardStorage {
@TransportType(() => Buffer)
private hashValues: Buffer;
@TransportType(() => Buffer)
ocgcoreWasmBinary?: Buffer;
private hashMask: number;
size: number;
......@@ -32,17 +35,22 @@ export class CardStorage {
entries: Buffer,
hashKeys: Buffer,
hashValues: Buffer,
ocgcoreWasmBinary: Buffer | undefined,
hashMask: number,
size: number,
) {
this.entries = entries;
this.hashKeys = hashKeys;
this.hashValues = hashValues;
this.ocgcoreWasmBinary = ocgcoreWasmBinary;
this.hashMask = hashMask;
this.size = size;
}
static fromCards(cards: Iterable<CardDataEntry>): CardStorage {
static fromCards(
cards: Iterable<CardDataEntry>,
ocgcoreWasmBinary?: Buffer,
): CardStorage {
const uniqueCards: CardDataEntry[] = [];
const seen = new Set<number>();
for (const card of cards) {
......@@ -63,6 +71,7 @@ export class CardStorage {
entries,
hashKeys,
hashValues,
ocgcoreWasmBinary,
hashCapacity - 1,
uniqueCards.length,
);
......
......@@ -8,9 +8,12 @@ import BetterLock from 'better-lock';
import { CardStorage } from './card-storage';
import { CardLoadWorker } from './card-load-worker';
const CARD_STORAGE_RELOAD_INTERVAL_MS = 10 * 60 * 1000;
export class YGOProResourceLoader {
constructor(private ctx: Context) {
void this.loadYGOProCdbs();
this.registerReloadTimer();
}
ygoproPaths = this.ctx.config
......@@ -25,8 +28,8 @@ export class YGOProResourceLoader {
private loadingLock = new BetterLock();
private loadingCardStorage?: Promise<CardStorage>;
private currentCardStorage?: CardStorage;
private currentCardReader?: CardReaderFn;
private currentOcgcoreWasmBinary?: Buffer;
private currentCardStorageSha512?: Buffer;
private reloadTimerRegistered = false;
async getCardStorage() {
if (this.currentCardStorage) {
......@@ -39,18 +42,13 @@ export class YGOProResourceLoader {
}
async getCardReader(): Promise<CardReaderFn> {
if (this.currentCardReader) {
return this.currentCardReader;
}
const storage = await this.getCardStorage();
const reader = storage.toCardReader();
this.currentCardReader = reader;
return reader;
return storage.toCardReader();
}
async getOcgcoreWasmBinary() {
await this.getCardStorage();
return this.currentOcgcoreWasmBinary;
const storage = await this.getCardStorage();
return storage.ocgcoreWasmBinary;
}
async loadYGOProCdbs() {
......@@ -58,9 +56,10 @@ export class YGOProResourceLoader {
return this.loadingCardStorage;
}
const loading = this.loadingLock.acquire(async () => {
const storage = await this.loadCardStorage();
const { cardStorage, sha512 } = await this.loadCardStorage();
const storage = cardStorage;
this.currentCardStorage = storage;
this.currentCardReader = storage.toCardReader();
this.currentCardStorageSha512 = sha512;
return storage;
});
this.loadingCardStorage = loading;
......@@ -73,21 +72,64 @@ export class YGOProResourceLoader {
}
}
private registerReloadTimer() {
if (this.reloadTimerRegistered) {
return;
}
this.reloadTimerRegistered = true;
setInterval(() => {
this.reloadYGOProCdbsIfChanged().catch((error) => {
this.logger.warn(
{ error },
'Failed reloading card storage by periodic refresh',
);
});
}, CARD_STORAGE_RELOAD_INTERVAL_MS);
}
private async reloadYGOProCdbsIfChanged() {
if (!this.currentCardStorage) {
await this.loadYGOProCdbs();
return;
}
if (this.loadingCardStorage) {
await this.loadingCardStorage;
return;
}
const loading = this.loadingLock.acquire(async () => {
const { cardStorage, sha512 } = await this.loadCardStorage();
if (this.currentCardStorageSha512?.equals(sha512)) {
this.logger.debug('Card storage hash unchanged, keeping current data');
return this.currentCardStorage!;
}
this.currentCardStorage = cardStorage;
this.currentCardStorageSha512 = sha512;
this.logger.info('Card storage hash changed, replaced current data');
return cardStorage;
});
this.loadingCardStorage = loading;
try {
await loading;
} finally {
if (this.loadingCardStorage === loading) {
this.loadingCardStorage = undefined;
}
}
}
private async loadCardStorage() {
const ocgcoreWasmPathConfig =
this.ctx.config.getString('OCGCORE_WASM_PATH');
const ocgcoreWasmPath = ocgcoreWasmPathConfig
? path.resolve(process.cwd(), ocgcoreWasmPathConfig)
: undefined;
const { cardStorage, dbCount, failedFiles, ocgcoreWasmBinary } =
await runInWorker(
CardLoadWorker,
(worker) => worker.load(),
this.ygoproPaths,
ocgcoreWasmPath,
);
const { cardStorage, dbCount, failedFiles, sha512 } = await runInWorker(
CardLoadWorker,
(worker) => worker.load(),
this.ygoproPaths,
ocgcoreWasmPath,
);
this.currentOcgcoreWasmBinary = ocgcoreWasmBinary;
for (const failedFile of failedFiles) {
this.logger.warn(`Failed to read ${failedFile}`);
}
......@@ -97,7 +139,10 @@ export class YGOProResourceLoader {
},
`Merged database from ${dbCount} databases with ${cardStorage.size} cards`,
);
return cardStorage;
return {
cardStorage,
sha512,
};
}
async *getLFLists() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment