Skip to content

Commit

Permalink
Fix path validation bug
Browse files Browse the repository at this point in the history
  • Loading branch information
FLSoz committed May 29, 2022
1 parent 32d4f82 commit 57ebbac
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 90 deletions.
12 changes: 11 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"javascript.format.enable": false,
"typescript.format.enable": false,

"editor.formatOnSave": true,

"search.exclude": {
".git": true,
".eslintcache": true,
Expand All @@ -19,5 +21,13 @@
"test/**/__snapshots__": true,
"package-lock.json": true,
"*.{css,sass,scss}.d.ts": true
}
},
"prettier.configPath": "",
"prettier.useTabs": true,
"prettier.singleQuote": true,
"prettier.documentSelectors": [
"*.js",
"*.ts",
"*.tsx"
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "terratech-steam-mod-loader",
"productName": "ttsm",
"description": "Mod loader for TerraTech that handles Steam Workshop mod configurations",
"version": "1.5.3",
"version": "1.5.4",
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
Expand Down
2 changes: 1 addition & 1 deletion release/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "terratech-steam-mod-loader",
"productName": "ttsm",
"description": "Mod loader for TerraTech that handles Steam Workshop mod configurations",
"version": "1.5.3",
"version": "1.5.4",
"main": "./dist/main/main.js",
"author": {
"name": "FLSoz",
Expand Down
18 changes: 10 additions & 8 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import fs from 'fs';
import child_process from 'child_process';
import psList from 'ps-list';

import { ModData, ModCollection, ModType, SessionMods, ValidChannel, AppConfig, ModErrorType } from '../model';
import { ModData, ModCollection, ModType, SessionMods, ValidChannel, AppConfig, ModErrorType, PathType, PathParams } from '../model';
import Steamworks, { EResult, UGCItemState } from './steamworks';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
Expand Down Expand Up @@ -175,11 +175,6 @@ ipcMain.on(ValidChannel.CLOSE, () => {
}
});

interface PathParams {
prefixes: string[];
path: string;
}

ipcMain.on(ValidChannel.OPEN_MOD_STEAM, async (event, workshopID: bigint) => {
shell.openExternal(`steam://url/CommunityFilePage/${workshopID}`);
});
Expand Down Expand Up @@ -543,7 +538,7 @@ ipcMain.handle(ValidChannel.LIST_SUBDIRS, async (_event, pathParams: PathParams)
}
});

// Check if path exists
// Mkdir
ipcMain.handle(ValidChannel.MKDIR, async (_event, pathParams: PathParams) => {
const filepath = path.join(...pathParams.prefixes, pathParams.path);
log.info(`Mkdir ${filepath}`);
Expand Down Expand Up @@ -572,7 +567,14 @@ ipcMain.handle(ValidChannel.READ_FILE, async (_event, pathParams: PathParams) =>
ipcMain.handle(ValidChannel.PATH_EXISTS, async (_event, pathParams: PathParams) => {
const filepath = path.join(...pathParams.prefixes, pathParams.path);
try {
return fs.existsSync(filepath);
const stats = fs.statSync(filepath);
if (pathParams.type === PathType.DIRECTORY) {
return stats.isDirectory();
}
if (pathParams.type === PathType.FILE) {
return stats.isFile();
}
return true;
} catch (error) {
log.error(error);
return false;
Expand Down
6 changes: 6 additions & 0 deletions src/model/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ export enum ProgressTypes {
MOD_LOAD = 'mod-load'
}

export enum PathType {
FILE,
DIRECTORY
}

export interface PathParams {
prefixes: string[];
path: string;
type?: PathType;
}

export enum LogLevel {
Expand Down
15 changes: 11 additions & 4 deletions src/model/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ import { CollectionConfig } from './CollectionConfig';
import { ModErrorType } from './CollectionValidation';
import { MainCollectionConfig } from './MainCollectionView';

export enum AppConfigKeys {
LOCAL_DIR = 'localDir',
GAME_EXEC = 'gameExec',
LOGS_DIR = 'logsDir',
MANAGER_ID = 'workshopID'
}

export interface AppConfig {
closeOnLaunch: boolean;

language: string;

localDir?: string;
gameExec: string;
workshopID: bigint;
[AppConfigKeys.LOCAL_DIR]?: string;
[AppConfigKeys.GAME_EXEC]: string;
[AppConfigKeys.MANAGER_ID]: bigint;

activeCollection?: string;
extraParams?: string;

logLevel?: LogLevel;
logsDir: string;
[AppConfigKeys.LOGS_DIR]: string;

steamMaxConcurrency: number;
currentPath: string;
Expand Down
13 changes: 8 additions & 5 deletions src/renderer/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/no-explicit-any */

import { AppConfig, ModCollection, ModData, ValidChannel, PathParams } from 'model';
import { AppConfig, ModCollection, ModData, ValidChannel, PathParams, PathType } from 'model';

interface ElectronInterface {
platform: string;
Expand Down Expand Up @@ -140,12 +140,15 @@ class API {
}

// file API
convertToPathParam(path: PathParams | string): PathParams {
convertToPathParam(path: PathParams | string, type?: PathType): PathParams {
let pathParams: PathParams;
if (typeof path === 'string') {
pathParams = { prefixes: [], path };
pathParams = { prefixes: [], path, type };
} else {
pathParams = path;
if (type !== undefined) {
pathParams.type = type;
}
}
return pathParams;
}
Expand Down Expand Up @@ -178,8 +181,8 @@ class API {
return ipcRenderer.invoke(ValidChannel.MKDIR, this.convertToPathParam(path));
}

pathExists(path: PathParams | string): Promise<any> {
return ipcRenderer.invoke(ValidChannel.PATH_EXISTS, this.convertToPathParam(path));
pathExists(path: PathParams | string, type?: PathType): Promise<any> {
return ipcRenderer.invoke(ValidChannel.PATH_EXISTS, this.convertToPathParam(path, type));
}

access(path: PathParams | string): Promise<any> {
Expand Down
17 changes: 11 additions & 6 deletions src/renderer/components/loading/ConfigLoading.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import React, { Component } from 'react';
import api from 'renderer/Api';
import { AppConfig, ModCollection, AppState, ValidChannel } from 'model';
import { AppConfig, ModCollection, AppState, ValidChannel, AppConfigKeys } from 'model';
import { Layout, Progress } from 'antd';
import { useNavigate, NavigateFunction, useOutletContext } from 'react-router-dom';
import { DEFAULT_CONFIG } from 'renderer/Constants';
import { validateSettingsPath } from 'util/Validation';

const { Footer, Content } = Layout;

async function validateAppConfig(config: AppConfig): Promise<{ [field: string]: string } | undefined> {
const errors: { [field: string]: string } = {};
const fields: ('gameExec' | 'localDir')[] = ['gameExec', 'localDir'];
const paths = ['Steam executable', 'TerraTech Steam Workshop directory', 'TerraTech Local Mods directory'];
const fields: AppConfigKeys[] = [AppConfigKeys.GAME_EXEC, AppConfigKeys.LOCAL_DIR];
const paths = ['Steam executable', 'TerraTech Local Mods directory', 'TerraTech Steam Workshop directory'];
let failed = false;
await Promise.allSettled([api.pathExists(config.gameExec), config.localDir ? api.pathExists(config.localDir) : true]).then((results) => {
await Promise.allSettled([
validateSettingsPath('gameExec', config.gameExec),
config.localDir && config.localDir.length > 0 ? validateSettingsPath('localDir', config.localDir) : undefined
]).then((results) => {
results.forEach((result, index) => {
if (result.status !== 'fulfilled') {
errors[fields[index]] = `Unexpected error checking ${fields[index]} path (${paths[index]})`;
failed = true;
} else if (!result.value) {
errors[fields[index]] = `Path to ${fields[index]} (${paths[index]}) was invalid`;
} else if (result.value !== undefined) {
errors[fields[index]] = result.value;
failed = true;
}
});

return failed;
});

Expand Down
78 changes: 15 additions & 63 deletions src/renderer/views/SettingsView.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { Component } from 'react';
import { AppState, AppConfig, ValidChannel, LogLevel } from 'model';
import { Layout, Form, Input, InputNumber, Switch, Button, FormInstance, Space, PageHeader, Select, Row, Col, Typography } from 'antd';
import { AppState, AppConfig, ValidChannel, LogLevel, AppConfigKeys } from 'model';
import { Layout, Form, Input, InputNumber, Switch, Button, FormInstance, Space, PageHeader, Select, Row, Col } from 'antd';
import { useOutletContext } from 'react-router-dom';
import api from 'renderer/Api';
import { FolderOutlined } from '@ant-design/icons';
import { TT_APP_ID } from 'renderer/Constants';
import { validateSettingsPath } from 'util/Validation';

const { Paragraph, Text, Title } = Typography;
const { Content } = Layout;
const { Search } = Input;

Expand Down Expand Up @@ -59,7 +58,7 @@ class SettingsView extends Component<AppState, SettingsState> {
api.removeAllListeners(ValidChannel.SELECT_PATH);
}

setSelectedPath(path: string, target: 'localDir' | 'gameExec') {
setSelectedPath(path: string, target: AppConfigKeys.LOCAL_DIR | AppConfigKeys.LOGS_DIR | AppConfigKeys.GAME_EXEC) {
if (path) {
const { editingConfig } = this.state;
editingConfig![target] = path;
Expand Down Expand Up @@ -113,66 +112,19 @@ class SettingsView extends Component<AppState, SettingsState> {
validateFile(field: string, value: string) {
const { configErrors, updateState } = this.props;
if (!!value && value.length > 0) {
return api
.pathExists(value)
.catch((error) => {
api.logger.error(error);
configErrors[field] = error.toString();
return validateSettingsPath(field, value)
.then((error: string | undefined) => {
if (error !== undefined) {
configErrors[field] = error;
} else {
delete configErrors[field];
}
updateState({});
throw new Error(`Error while validating path:\n${error.toString()}`);
return !!error;
})
.then((success) => {
if (!success) {
configErrors[field] = 'Provided path is invalid';
updateState({});
throw new Error('Provided path is invalid');
}
switch (field) {
case 'gameExec':
if (value.toLowerCase().includes('terratech')) {
delete configErrors[field];
updateState({});
return true;
}
configErrors[field] = "The TerraTech executable should contain 'TerraTech'";
updateState({});
return false;

case 'localDir':
if (!value || value.toLowerCase().endsWith('localmods')) {
delete configErrors[field];
updateState({});
return true;
}
configErrors[field] = "The local mods directory should end with 'TerraTech/LocalMods'";
updateState({});
return false;

case 'workshopDir':
if (value.endsWith(TT_APP_ID)) {
delete configErrors[field];
updateState({});
return true;
}
configErrors[field] = `The workshop directory should end with TT app ID 'Steam/steamapps/workshop/content/${TT_APP_ID}'`;
updateState({});
return false;

case 'logsDir':
if (value.toLowerCase().includes('logs')) {
delete configErrors[field];
updateState({});
return true;
}
configErrors[field] = "The logs directory should contain 'Logs'";
updateState({});
return false;

default:
delete configErrors[field];
updateState({});
return true;
}
.catch((err) => {
configErrors[field] = err.toString();
updateState({});
});
}
if (field === 'localDir') {
Expand Down
7 changes: 6 additions & 1 deletion src/util/Sleep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export interface ForEachProps<Type> {
array: Type[];
}

function delayForEach<Type>(array: Type[], delayTime: number, func: (props: ForEachProps<Type>, ...funcArgs: any[]) => void, ...args: any[]): Promise<any> {
function delayForEach<Type>(
array: Type[],
delayTime: number,
func: (props: ForEachProps<Type>, ...funcArgs: any[]) => void,
...args: any[]
): Promise<any> {
let promise = Promise.resolve();
let index = 0;
while (index < array.length) {
Expand Down
45 changes: 45 additions & 0 deletions src/util/Validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PathType } from 'model';
import api from 'renderer/Api';
import { platform, TT_APP_ID } from 'renderer/Constants';

// eslint-disable-next-line import/prefer-default-export
export async function validateSettingsPath(field: string, value: string): Promise<string | undefined> {
const result: string | undefined = await api
.pathExists(value, field === 'gameExec' ? PathType.FILE : PathType.DIRECTORY)
.then((success) => {
if (!success) {
return 'Provided path is invalid';
}
switch (field) {
case 'gameExec':
if (value.toLowerCase().includes('terratech')) {
if (platform === 'win32' && !value.endsWith('.exe')) {
return 'Windows executables must end in .exe';
}
return undefined;
}
return "The TerraTech executable should contain 'TerraTech'";
case 'localDir':
if (!value || value.toLowerCase().endsWith('localmods')) {
return undefined;
}
return "The local mods directory should end with 'TerraTech/LocalMods'";
case 'workshopDir':
if (value.endsWith(TT_APP_ID)) {
return undefined;
}
return `The workshop directory should end with TT app ID 'Steam/steamapps/workshop/content/${TT_APP_ID}'`;
case 'logsDir':
if (value.toLowerCase().includes('logs')) {
return undefined;
}
return "The logs directory should contain 'Logs'";
default:
return undefined;
}
})
.catch((error) => {
return error.toString();
});
return result;
}
23 changes: 23 additions & 0 deletions terratech-steam-mod-loader.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"files.associations": {
".eslintrc": "jsonc",
".prettierrc": "jsonc",
".eslintignore": "ignore"
},
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.format.enable": false,
"prettier.enable": true,
"prettier.documentSelectors": [
"*.js",
"*.ts",
"*.tsx"
]
}
}

0 comments on commit 57ebbac

Please sign in to comment.