2023-03-03 16:23:46 +01:00
|
|
|
import fixJson from "json-fixer";
|
2023-08-09 12:49:45 +02:00
|
|
|
import { jsonc } from "jsonc";
|
|
|
|
import { IParseOptions, IStringifyOptions, Reviver } from "jsonc/lib/interfaces";
|
2023-03-03 16:23:46 +01:00
|
|
|
import { inject, injectable } from "tsyringe";
|
|
|
|
import { ILogger } from "../models/spt/utils/ILogger";
|
|
|
|
import { HashUtil } from "./HashUtil";
|
|
|
|
import { VFS } from "./VFS";
|
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class JsonUtil
|
|
|
|
{
|
|
|
|
protected fileHashes = null;
|
|
|
|
protected jsonCacheExists = false;
|
2023-08-09 12:49:45 +02:00
|
|
|
protected jsonCachePath = "./user/cache/jsonCache.json";
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
@inject("VFS") protected vfs: VFS,
|
|
|
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
|
|
|
@inject("WinstonLogger") protected logger: ILogger
|
|
|
|
)
|
|
|
|
{ }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* From object to string
|
|
|
|
* @param data object to turn into JSON
|
2023-08-09 12:49:45 +02:00
|
|
|
* @param prettify Should output be prettified
|
2023-03-03 16:23:46 +01:00
|
|
|
* @returns string
|
|
|
|
*/
|
2023-08-09 12:49:45 +02:00
|
|
|
public serialize(data: any, prettify = false): string
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
|
|
|
if (prettify)
|
|
|
|
{
|
|
|
|
return JSON.stringify(data, null, "\t");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return JSON.stringify(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-09 12:49:45 +02:00
|
|
|
/**
|
|
|
|
* From object to string
|
|
|
|
* @param data object to turn into JSON
|
|
|
|
* @param replacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.
|
|
|
|
* @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
|
|
|
|
* @returns string
|
|
|
|
*/
|
|
|
|
public serializeAdvanced(data: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string
|
|
|
|
{
|
|
|
|
|
|
|
|
return JSON.stringify(data, replacer, space);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* From object to string
|
|
|
|
* @param data object to turn into JSON
|
|
|
|
* @param filename Name of file being serialized
|
|
|
|
* @param options Stringify options or a replacer.
|
|
|
|
* @returns The string converted from the JavaScript value
|
|
|
|
*/
|
|
|
|
public serializeJsonC(
|
|
|
|
data: any,
|
|
|
|
filename?: string | null,
|
|
|
|
options?: IStringifyOptions | Reviver): string
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return jsonc.stringify(data, options);
|
|
|
|
}
|
|
|
|
catch (error)
|
|
|
|
{
|
|
|
|
this.logger.error(`unable to stringify jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
/**
|
|
|
|
* From string to object
|
|
|
|
* @param jsonString json string to turn into object
|
2023-08-09 12:49:45 +02:00
|
|
|
* @param filename Name of file being deserialized
|
2023-03-03 16:23:46 +01:00
|
|
|
* @returns object
|
|
|
|
*/
|
|
|
|
public deserialize<T>(jsonString: string, filename = ""): T
|
|
|
|
{
|
2023-08-09 12:49:45 +02:00
|
|
|
try
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2023-08-09 12:49:45 +02:00
|
|
|
return JSON.parse(jsonString);
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
2023-08-09 12:49:45 +02:00
|
|
|
catch (error)
|
|
|
|
{
|
|
|
|
this.logger.error(`unable to parse json file: ${filename} message: ${error.message}, stack: ${error.stack}`);
|
|
|
|
}
|
|
|
|
}
|
2023-03-03 16:23:46 +01:00
|
|
|
|
2023-08-09 12:49:45 +02:00
|
|
|
/**
|
|
|
|
* From string to object
|
|
|
|
* @param jsonString json string to turn into object
|
|
|
|
* @param filename Name of file being deserialized
|
|
|
|
* @param options Parsing options
|
|
|
|
* @returns object
|
|
|
|
*/
|
|
|
|
public deserializeJsonC<T>(jsonString: string, filename = "", options?: IParseOptions): T
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return jsonc.parse(jsonString, options);
|
|
|
|
}
|
|
|
|
catch (error)
|
|
|
|
{
|
|
|
|
this.logger.error(`unable to parse jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public async deserializeWithCacheCheckAsync<T>(jsonString: string, filePath: string): Promise<T>
|
|
|
|
{
|
|
|
|
return new Promise((resolve) =>
|
|
|
|
{
|
|
|
|
resolve(this.deserializeWithCacheCheck<T>(jsonString, filePath));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-09 12:49:45 +02:00
|
|
|
/**
|
|
|
|
* From json string to object
|
|
|
|
* @param jsonString String to turn into object
|
|
|
|
* @param filePath Path to json file being processed
|
|
|
|
* @returns Object
|
|
|
|
*/
|
2023-03-03 16:23:46 +01:00
|
|
|
public deserializeWithCacheCheck<T>(jsonString: string, filePath: string): T
|
|
|
|
{
|
2023-08-09 12:49:45 +02:00
|
|
|
this.ensureJsonCacheExists(this.jsonCachePath);
|
|
|
|
this.hydrateJsonCache(this.jsonCachePath);
|
2023-03-03 16:23:46 +01:00
|
|
|
|
|
|
|
// Generate hash of string
|
|
|
|
const generatedHash = this.hashUtil.generateSha1ForData(jsonString);
|
|
|
|
|
|
|
|
// Get hash of file and check if missing or hash mismatch
|
|
|
|
let savedHash = this.fileHashes[filePath];
|
|
|
|
if (!savedHash || savedHash !== generatedHash)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
const { data, changed } = fixJson(jsonString);
|
|
|
|
if (changed) // data invalid, return it
|
|
|
|
{
|
|
|
|
this.logger.error(`${filePath} - Detected faulty json, please fix your json file using VSCodium`);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// data valid, save hash and call function again
|
|
|
|
this.fileHashes[filePath] = generatedHash;
|
2023-08-09 12:49:45 +02:00
|
|
|
this.vfs.writeFile(this.jsonCachePath, this.serialize(this.fileHashes, true));
|
2023-03-03 16:23:46 +01:00
|
|
|
savedHash = generatedHash;
|
|
|
|
}
|
|
|
|
return data as T;
|
|
|
|
}
|
|
|
|
catch (error)
|
|
|
|
{
|
|
|
|
const errorMessage = `Attempted to parse file: ${filePath}. Error: ${error.message}`;
|
|
|
|
this.logger.error(errorMessage);
|
|
|
|
throw new Error(errorMessage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Doesn't match
|
|
|
|
if (savedHash !== generatedHash)
|
|
|
|
{
|
|
|
|
throw new Error(`Catastrophic failure processing file ${filePath}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Match!
|
2023-08-09 12:49:45 +02:00
|
|
|
return this.deserialize<T>(jsonString);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create file if nothing found
|
|
|
|
* @param jsonCachePath path to cache
|
|
|
|
*/
|
|
|
|
protected ensureJsonCacheExists(jsonCachePath: string): void
|
|
|
|
{
|
|
|
|
if (!this.jsonCacheExists)
|
|
|
|
{
|
|
|
|
if (!this.vfs.exists(jsonCachePath))
|
|
|
|
{
|
|
|
|
// Create empty object at path
|
|
|
|
this.vfs.writeFile(jsonCachePath, "{}");
|
|
|
|
}
|
|
|
|
this.jsonCacheExists = true;
|
|
|
|
}
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
|
2023-08-09 12:49:45 +02:00
|
|
|
/**
|
|
|
|
* Read contents of json cache and add to class field
|
|
|
|
* @param jsonCachePath Path to cache
|
|
|
|
*/
|
|
|
|
protected hydrateJsonCache(jsonCachePath: string) : void
|
|
|
|
{
|
|
|
|
// Get all file hashes
|
|
|
|
if (!this.fileHashes)
|
|
|
|
{
|
|
|
|
this.fileHashes = this.deserialize(this.vfs.readFile(`${jsonCachePath}`));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert into string and back into object to clone object
|
|
|
|
* @param objectToClone Item to clone
|
|
|
|
* @returns Cloned parameter
|
|
|
|
*/
|
|
|
|
public clone<T>(objectToClone: T): T
|
2023-03-03 16:23:46 +01:00
|
|
|
{
|
2023-08-09 12:49:45 +02:00
|
|
|
return this.deserialize<T>(this.serialize(objectToClone));
|
2023-03-03 16:23:46 +01:00
|
|
|
}
|
|
|
|
}
|