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 * as fs from 'node:fs';
import { createHash, Hash } from 'node:crypto';
import { searchYGOProResource } from 'koishipro-core.js'; import { searchYGOProResource } from 'koishipro-core.js';
import type { CardDataEntry } from 'ygopro-cdb-encode'; import type { CardDataEntry } from 'ygopro-cdb-encode';
import { YGOProCdb } from 'ygopro-cdb-encode'; import { YGOProCdb } from 'ygopro-cdb-encode';
...@@ -12,6 +13,17 @@ const isFileNotFoundError = (error: unknown): error is NodeJS.ErrnoException => ...@@ -12,6 +13,17 @@ const isFileNotFoundError = (error: unknown): error is NodeJS.ErrnoException =>
'code' in error && 'code' in error &&
(error as NodeJS.ErrnoException).code === 'ENOENT'; (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 { export class CardLoadWorkerResult {
@TransportType(() => CardStorage) @TransportType(() => CardStorage)
cardStorage: CardStorage; cardStorage: CardStorage;
...@@ -20,7 +32,7 @@ export class CardLoadWorkerResult { ...@@ -20,7 +32,7 @@ export class CardLoadWorkerResult {
failedFiles: string[]; failedFiles: string[];
@TransportType(() => Buffer) @TransportType(() => Buffer)
ocgcoreWasmBinary?: Buffer; sha512: Buffer;
} }
@DefineWorker() @DefineWorker()
...@@ -38,6 +50,7 @@ export class CardLoadWorker { ...@@ -38,6 +50,7 @@ export class CardLoadWorker {
const seen = new Set<number>(); const seen = new Set<number>();
let dbCount = 0; let dbCount = 0;
const failedFiles: string[] = []; const failedFiles: string[] = [];
const sha512 = createHash('sha512');
for await (const file of searchYGOProResource(...this.ygoproPaths)) { for await (const file of searchYGOProResource(...this.ygoproPaths)) {
if (!file.path.endsWith('.cdb')) { if (!file.path.endsWith('.cdb')) {
...@@ -45,7 +58,8 @@ export class CardLoadWorker { ...@@ -45,7 +58,8 @@ export class CardLoadWorker {
} }
try { try {
const currentDb = new SQL.Database(await file.read()); const cdbBody = await file.read();
const currentDb = new SQL.Database(cdbBody);
try { try {
const currentCdb = new YGOProCdb(currentDb).noTexts(); const currentCdb = new YGOProCdb(currentDb).noTexts();
for (const card of currentCdb.step()) { for (const card of currentCdb.step()) {
...@@ -57,6 +71,8 @@ export class CardLoadWorker { ...@@ -57,6 +71,8 @@ export class CardLoadWorker {
cards.push(card); cards.push(card);
} }
++dbCount; ++dbCount;
hashWithSizePrefixText(sha512, file.path);
hashWithSizePrefix(sha512, Buffer.from(cdbBody));
} finally { } finally {
currentDb.close(); currentDb.close();
} }
...@@ -69,7 +85,11 @@ export class CardLoadWorker { ...@@ -69,7 +85,11 @@ export class CardLoadWorker {
let ocgcoreWasmBinary: Buffer | undefined; let ocgcoreWasmBinary: Buffer | undefined;
if (this.ocgcoreWasmPath) { if (this.ocgcoreWasmPath) {
try { 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); ocgcoreWasmBinary = toShared(wasmBinary);
} catch (error) { } catch (error) {
if (!isFileNotFoundError(error)) { if (!isFileNotFoundError(error)) {
...@@ -79,10 +99,12 @@ export class CardLoadWorker { ...@@ -79,10 +99,12 @@ export class CardLoadWorker {
} }
const result = new CardLoadWorkerResult(); const result = new CardLoadWorkerResult();
result.cardStorage = toShared(CardStorage.fromCards(cards)); result.cardStorage = toShared(
CardStorage.fromCards(cards, ocgcoreWasmBinary),
);
result.dbCount = dbCount; result.dbCount = dbCount;
result.failedFiles = failedFiles; result.failedFiles = failedFiles;
result.ocgcoreWasmBinary = ocgcoreWasmBinary; result.sha512 = toShared(sha512.digest());
return result; return result;
} }
} }
...@@ -25,6 +25,9 @@ export class CardStorage { ...@@ -25,6 +25,9 @@ export class CardStorage {
@TransportType(() => Buffer) @TransportType(() => Buffer)
private hashValues: Buffer; private hashValues: Buffer;
@TransportType(() => Buffer)
ocgcoreWasmBinary?: Buffer;
private hashMask: number; private hashMask: number;
size: number; size: number;
...@@ -32,17 +35,22 @@ export class CardStorage { ...@@ -32,17 +35,22 @@ export class CardStorage {
entries: Buffer, entries: Buffer,
hashKeys: Buffer, hashKeys: Buffer,
hashValues: Buffer, hashValues: Buffer,
ocgcoreWasmBinary: Buffer | undefined,
hashMask: number, hashMask: number,
size: number, size: number,
) { ) {
this.entries = entries; this.entries = entries;
this.hashKeys = hashKeys; this.hashKeys = hashKeys;
this.hashValues = hashValues; this.hashValues = hashValues;
this.ocgcoreWasmBinary = ocgcoreWasmBinary;
this.hashMask = hashMask; this.hashMask = hashMask;
this.size = size; this.size = size;
} }
static fromCards(cards: Iterable<CardDataEntry>): CardStorage { static fromCards(
cards: Iterable<CardDataEntry>,
ocgcoreWasmBinary?: Buffer,
): CardStorage {
const uniqueCards: CardDataEntry[] = []; const uniqueCards: CardDataEntry[] = [];
const seen = new Set<number>(); const seen = new Set<number>();
for (const card of cards) { for (const card of cards) {
...@@ -63,6 +71,7 @@ export class CardStorage { ...@@ -63,6 +71,7 @@ export class CardStorage {
entries, entries,
hashKeys, hashKeys,
hashValues, hashValues,
ocgcoreWasmBinary,
hashCapacity - 1, hashCapacity - 1,
uniqueCards.length, uniqueCards.length,
); );
......
...@@ -8,9 +8,12 @@ import BetterLock from 'better-lock'; ...@@ -8,9 +8,12 @@ import BetterLock from 'better-lock';
import { CardStorage } from './card-storage'; import { CardStorage } from './card-storage';
import { CardLoadWorker } from './card-load-worker'; import { CardLoadWorker } from './card-load-worker';
const CARD_STORAGE_RELOAD_INTERVAL_MS = 10 * 60 * 1000;
export class YGOProResourceLoader { export class YGOProResourceLoader {
constructor(private ctx: Context) { constructor(private ctx: Context) {
void this.loadYGOProCdbs(); void this.loadYGOProCdbs();
this.registerReloadTimer();
} }
ygoproPaths = this.ctx.config ygoproPaths = this.ctx.config
...@@ -25,8 +28,8 @@ export class YGOProResourceLoader { ...@@ -25,8 +28,8 @@ export class YGOProResourceLoader {
private loadingLock = new BetterLock(); private loadingLock = new BetterLock();
private loadingCardStorage?: Promise<CardStorage>; private loadingCardStorage?: Promise<CardStorage>;
private currentCardStorage?: CardStorage; private currentCardStorage?: CardStorage;
private currentCardReader?: CardReaderFn; private currentCardStorageSha512?: Buffer;
private currentOcgcoreWasmBinary?: Buffer; private reloadTimerRegistered = false;
async getCardStorage() { async getCardStorage() {
if (this.currentCardStorage) { if (this.currentCardStorage) {
...@@ -39,18 +42,13 @@ export class YGOProResourceLoader { ...@@ -39,18 +42,13 @@ export class YGOProResourceLoader {
} }
async getCardReader(): Promise<CardReaderFn> { async getCardReader(): Promise<CardReaderFn> {
if (this.currentCardReader) {
return this.currentCardReader;
}
const storage = await this.getCardStorage(); const storage = await this.getCardStorage();
const reader = storage.toCardReader(); return storage.toCardReader();
this.currentCardReader = reader;
return reader;
} }
async getOcgcoreWasmBinary() { async getOcgcoreWasmBinary() {
await this.getCardStorage(); const storage = await this.getCardStorage();
return this.currentOcgcoreWasmBinary; return storage.ocgcoreWasmBinary;
} }
async loadYGOProCdbs() { async loadYGOProCdbs() {
...@@ -58,9 +56,10 @@ export class YGOProResourceLoader { ...@@ -58,9 +56,10 @@ export class YGOProResourceLoader {
return this.loadingCardStorage; return this.loadingCardStorage;
} }
const loading = this.loadingLock.acquire(async () => { const loading = this.loadingLock.acquire(async () => {
const storage = await this.loadCardStorage(); const { cardStorage, sha512 } = await this.loadCardStorage();
const storage = cardStorage;
this.currentCardStorage = storage; this.currentCardStorage = storage;
this.currentCardReader = storage.toCardReader(); this.currentCardStorageSha512 = sha512;
return storage; return storage;
}); });
this.loadingCardStorage = loading; this.loadingCardStorage = loading;
...@@ -73,21 +72,64 @@ export class YGOProResourceLoader { ...@@ -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() { private async loadCardStorage() {
const ocgcoreWasmPathConfig = const ocgcoreWasmPathConfig =
this.ctx.config.getString('OCGCORE_WASM_PATH'); this.ctx.config.getString('OCGCORE_WASM_PATH');
const ocgcoreWasmPath = ocgcoreWasmPathConfig const ocgcoreWasmPath = ocgcoreWasmPathConfig
? path.resolve(process.cwd(), ocgcoreWasmPathConfig) ? path.resolve(process.cwd(), ocgcoreWasmPathConfig)
: undefined; : undefined;
const { cardStorage, dbCount, failedFiles, ocgcoreWasmBinary } = const { cardStorage, dbCount, failedFiles, sha512 } = await runInWorker(
await runInWorker( CardLoadWorker,
CardLoadWorker, (worker) => worker.load(),
(worker) => worker.load(), this.ygoproPaths,
this.ygoproPaths, ocgcoreWasmPath,
ocgcoreWasmPath, );
);
this.currentOcgcoreWasmBinary = ocgcoreWasmBinary;
for (const failedFile of failedFiles) { for (const failedFile of failedFiles) {
this.logger.warn(`Failed to read ${failedFile}`); this.logger.warn(`Failed to read ${failedFile}`);
} }
...@@ -97,7 +139,10 @@ export class YGOProResourceLoader { ...@@ -97,7 +139,10 @@ export class YGOProResourceLoader {
}, },
`Merged database from ${dbCount} databases with ${cardStorage.size} cards`, `Merged database from ${dbCount} databases with ${cardStorage.size} cards`,
); );
return cardStorage; return {
cardStorage,
sha512,
};
} }
async *getLFLists() { 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