Skip to content

Commit f9f5b38

Browse files
committed
refactor config file validation
1 parent 7374b0a commit f9f5b38

19 files changed

+438
-875
lines changed

src/config/Config.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,50 +15,44 @@ export namespace Config {
1515
target?: string;
1616
}
1717
export interface AlgorithmColumns {
18-
to_translate: string[];
18+
process: string[];
1919
static: string[];
2020
reference: string[];
2121
}
22+
export type StringBasedSalt = { source: 'STRING'; value: string }
23+
export type FileBasedSalt = {
24+
source: 'FILE';
25+
validator_regex?: string;
26+
value: {
27+
win32?: string;
28+
darwin?: string;
29+
linux?: string;
30+
}
31+
};
2232
export interface Options {
2333
isBackup?: boolean;
2434
meta: {
2535
region: string;
2636
version: string;
37+
signature: string;
2738
};
2839
messages?: {
2940
terms_and_conditions: string;
3041
error_in_config: string;
3142
error_in_salt: string;
3243
};
3344
source: ColumnMap;
34-
validations: { [key: string]: ColumnValidation[] };
45+
validations?: { [key: string]: ColumnValidation[] };
3546
algorithm: {
3647
columns: AlgorithmColumns;
3748
hash: {
38-
strategy: 'SHA256' | 'SCRYPT' | 'ARGON2';
39-
num_iterations?: number;
40-
parallelism?: number;
41-
block_size?: number;
42-
size?: number;
43-
};
44-
salt: {
45-
source: 'FILE' | 'STRING';
46-
validator_regex: string;
47-
value:
48-
| {
49-
win32?: string;
50-
darwin?: string;
51-
linux?: string;
52-
}
53-
| string;
49+
strategy: 'SHA256'
5450
};
51+
salt: StringBasedSalt | FileBasedSalt
5552
};
5653
destination: ColumnMap;
5754
destination_map: ColumnMap;
5855
destination_errors: ColumnMap;
59-
signature: {
60-
config_signature: string;
61-
};
6256
}
6357
}
6458

src/config/configStore.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,13 @@ import {
2727
saveAppConfig,
2828
DEFAULT_APP_CONFIG,
2929
} from './appConfig.js';
30-
import { appDataLocation } from './utils.js';
3130

3231
import type { AppConfigData, Config } from './Config.js';
3332

34-
const __dirname = dirname(fileURLToPath(import.meta.url));
35-
3633
// Ensure the application's config file directory exists
3734
function ensureAppDirectoryExists(appDir: string) {
3835
if (!fs.existsSync(appDir)) {
39-
log("Application directory '", appDir, "' does not exits -- creating it");
36+
console.log("Application directory '", appDir, "' does not exits -- creating it");
4037
fs.mkdirSync(appDir /*, { recursive: true } */);
4138
}
4239
}
@@ -49,7 +46,7 @@ function saveConfig(configData: Config.Options, outputPath: string) {
4946
// update the config hash on import to account for the
5047
const outputData = JSON.stringify(configData, null, ' ');
5148
fs.writeFileSync(outputPath, outputData, CONFIG_FILE_ENCODING);
52-
log('Written config data to ', outputPath);
49+
console.log('Written config data to ', outputPath);
5350
}
5451

5552
interface ConfigStorePaths {
@@ -120,12 +117,12 @@ export class ConfigStore {
120117
// if the load succesds we have a valid config -- use it as a
121118
// user-provided one
122119
if (userConfigLoad.success) {
123-
log('User config validation success - using it as config');
120+
console.log('User config validation success - using it as config');
124121
this.useUserConfig(userConfigLoad.config, userConfigLoad.lastUpdated);
125122
return;
126123
}
127124

128-
log(
125+
console.log(
129126
'User config validation not successful - attempting to load backup config',
130127
);
131128
// if the default config load failed use the backup default
@@ -138,7 +135,7 @@ export class ConfigStore {
138135
// if the load succesds we have a valid config -- use it as
139136
// a config-from-backup
140137
if (backupConfigLoad.success) {
141-
log('Backup config validation success - using it as config');
138+
console.log('Backup config validation success - using it as config');
142139
backupConfigLoad.config.isBackup = true;
143140
this.useBackupConfig(backupConfigLoad.config);
144141
return;
@@ -148,7 +145,7 @@ export class ConfigStore {
148145
this.loadError = backupConfigLoad.error;
149146

150147
// if the backup config fails to load we are screwed
151-
log(
148+
console.log(
152149
'Backup config load failed - the application should alert the user: ',
153150
backupConfigLoad.error,
154151
);
@@ -193,26 +190,26 @@ export class ConfigStore {
193190
// This method does not save the backup as the user config, only deletes the user config file
194191
removeUserConfig() {
195192
// attempt to load the backup config
196-
log(
193+
console.log(
197194
'[removeUserConfig] Attempting to remove user configuration and replace with backup.',
198195
);
199196

200197
// if the current config is already a backup config don't do anything
201198
if (this.isCurrentConfigBackup()) {
202-
log('[removeUserConfig] Already using a backup config -- bailing');
199+
console.log('[removeUserConfig] Already using a backup config -- bailing');
203200
// this is not an error - we're already using the backup
204201
return;
205202
}
206203

207-
log('[removeUserConfig] Trying to load backup config file');
204+
console.log('[removeUserConfig] Trying to load backup config file');
208205
const backupConfigLoad = loadConfig(
209206
this.getBackupConfigFilePath(),
210207
this.getRegion(),
211208
);
212209

213210
// if failed return the error message (do not delete the user config yet)
214211
if (!backupConfigLoad.success) {
215-
log(
212+
console.log(
216213
'Backup config validation failed -- returning error and keeping existing user config:',
217214
backupConfigLoad.error,
218215
);
@@ -223,7 +220,7 @@ export class ConfigStore {
223220
}
224221

225222
// if successful use the loaded backup configuration
226-
log(
223+
console.log(
227224
'[removeUserConfig] Backup config validation success - using it as config',
228225
);
229226
backupConfigLoad.config.isBackup = true;
@@ -266,7 +263,7 @@ export class ConfigStore {
266263
// deletes the user configuration file
267264
_deleteUserConfigFile() {
268265
const configPath = this.getConfigFilePath();
269-
log('Deleting config file: ', configPath);
266+
console.log('Deleting config file: ', configPath);
270267
fs.unlinkSync(configPath);
271268
}
272269

src/config/generateConfigHash.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,32 @@ import type { Config } from './Config.js';
2222
const DEFAULT_HASH_TYPE = 'md5';
2323
const HASH_DIGEST_TYPE = 'hex';
2424

25+
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
26+
type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]>}
27+
type RecursivePartialExcept<T, K extends keyof T> = RecursivePartial<T> & Pick<T, K>
28+
2529
// Takes a config, removes the "signature" and salt keys from it, generates
2630
// a stable JSON representation and hashes it using the provided algorithm
2731
export function generateConfigHash(
2832
config: Config.Options,
2933
hashType = DEFAULT_HASH_TYPE,
3034
) {
3135
// create a nested copy of the object
32-
const configCopy = JSON.parse(JSON.stringify(config));
36+
const configCopy = { ...JSON.parse(JSON.stringify(config)) as RecursivePartial<Config.Options>};
3337

3438
// remove the "signature" key
35-
delete configCopy.signature;
39+
delete configCopy.meta!.signature;
3640

3741
// remove the "messages" key
3842
// TODO: messages should go in a separate locales file to future proof translations
3943
delete configCopy.messages;
4044

4145
// remove the "algorithm.salt" part as it may have injected keys
4246
// TODO: this enables messing with the salt file path pre-injection without signature validations, but is required for compatibility w/ the injection workflow
43-
delete configCopy.algorithm.salt.value;
47+
delete configCopy.algorithm!.salt!.value;
4448
// mock the salt source as STRING to ensure that both imported and saved
4549
// (with pre-injected salt) config files work
46-
configCopy.algorithm.salt.source = 'STRING';
50+
configCopy.algorithm!.salt!.source = 'STRING';
4751

4852
// generate a stable JSON representation
4953
const stableJson = stableStringify(configCopy);

src/config/loadConfig.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,25 @@ export function loadConfig(
8383
log('CONFIG HASH:', configHash);
8484

8585
// fail if the signature is not OK
86-
if (configHash !== configData.signature.config_signature) {
86+
if (configHash !== configData.meta.signature) {
8787
return {
8888
success: false,
89-
error: `Configuration file signature mismatch -- required signature is '${configData.signature.config_signature}' but user configuration has '${configHash}' `,
89+
error: `Configuration file signature mismatch -- required signature is '${configData.meta.signature}' but user configuration has '${configHash}' `,
9090
};
9191
}
9292

93-
// alphabetically sort the to_translate, static, and reference fields to standardise and prevent
93+
// alphabetically sort the process, static, and reference fields to standardise and prevent
9494
// different ordering in config producing different results in output.
95-
configData.algorithm.columns.to_translate =
96-
configData.algorithm.columns.to_translate.sort();
95+
configData.algorithm.columns.process =
96+
configData.algorithm.columns.process.sort();
9797
configData.algorithm.columns.reference =
9898
configData.algorithm.columns.reference.sort();
9999
configData.algorithm.columns.static =
100100
configData.algorithm.columns.static.sort();
101101

102102
// check if we need to inject the salt data into the config
103103
// if not, the config loading is finished
104-
if (configData.algorithm.salt.source.toUpperCase() !== 'FILE') {
104+
if (configData.algorithm.salt.source === 'STRING') {
105105
return {
106106
success: true,
107107
lastUpdated: lastUpdateDate,
@@ -111,9 +111,9 @@ export function loadConfig(
111111

112112
// figure out the file path and the validation regexp
113113
const saltFilePath = configData.algorithm.salt.value;
114-
const saltFileValidatorRegexp = RegExp(
115-
configData.algorithm.salt.validator_regex,
116-
);
114+
const saltFileValidatorRegexp = configData.algorithm.salt.validator_regex
115+
? RegExp(configData.algorithm.salt.validator_regex)
116+
: undefined;
117117

118118
// attempt to load the salt file
119119
// saltFilePath must be { win32: string, darwin: string } at this stage since salt.source is guaranteed to be "FILE".
@@ -133,11 +133,10 @@ export function loadConfig(
133133
}
134134

135135
// replace the "FILE" with "STRING" amd embed the salt data
136+
configData.algorithm.salt = configData.algorithm.salt as unknown as Config.StringBasedSalt;
136137
configData.algorithm.salt.source = 'STRING';
137138
configData.algorithm.salt.value = saltData;
138139

139-
configData;
140-
141140
// return the freshly injected config
142141
return {
143142
success: true,

src/config/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function attemptToReadTOMLData<T>(
4343

4444
// handle TOML
4545
if (filePath.toLowerCase().endsWith('.toml')) {
46+
// TODO: better error handling for toml parsing (i.e. unsupported mixed array types)
4647
return toml.parse(fileData);
4748
}
4849

0 commit comments

Comments
 (0)