From bd56b26a5d95fafdff8589f6e6d67f06d88c49f9 Mon Sep 17 00:00:00 2001 From: Piotr Rogowski Date: Sat, 8 Jan 2022 21:51:09 +0100 Subject: [PATCH] Upload validation (#364) --- package-lock.json | 63 ++++++++ package.json | 1 + src/hooks/useStorage.ts | 2 +- src/pages/Diagnose.tsx | 9 +- src/pages/Upload.tsx | 153 +++++++++++++----- src/utils/ParserInterface.ts | 7 + src/utils/{storage.ts => Storage.ts} | 4 +- ...torageInterface.ts => StorageInterface.ts} | 0 src/utils/logs/LogParser.ts | 56 +++++++ src/utils/logs/TriggerLogsParser.ts | 28 +++- .../{browserStorage.ts => BrowserStorage.ts} | 2 +- 11 files changed, 271 insertions(+), 54 deletions(-) create mode 100644 src/utils/ParserInterface.ts rename src/utils/{storage.ts => Storage.ts} (80%) rename src/utils/{storageInterface.ts => StorageInterface.ts} (100%) create mode 100644 src/utils/logs/LogParser.ts rename src/utils/storage/{browserStorage.ts => BrowserStorage.ts} (92%) diff --git a/package-lock.json b/package-lock.json index 39615d2..f6dbd94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@reduxjs/toolkit": "^1.7.1", "@sentry/react": "^6.16.1", "@sentry/tracing": "^6.16.1", + "@speedy-tuner/ini": "^0.2.2", "@speedy-tuner/types": "^0.2.1", "antd": "^4.18.2", "easymde": "^2.15.0", @@ -3907,6 +3908,33 @@ "eslint-plugin-prettier": "^4.0.0" } }, + "node_modules/@speedy-tuner/ini": { + "version": "0.2.2", + "resolved": "https://npm.pkg.github.com/download/@speedy-tuner/ini/0.2.2/38755d6ecbf8f478233f2a40251989192659620e0195f62eb6f4a79b920b9a0e", + "integrity": "sha512-sktFLjNF7oZa9haK+a71Ebc6csJ5uC/Huba879WiANydWRF7iIPdFyL6sfp8gOfoOj+I/jhg1MB+1z972hKdWw==", + "license": "MIT", + "dependencies": { + "@speedy-tuner/types": "^0.2.1", + "js-yaml": "^4.1.0", + "parsimmon": "^1.18.1" + } + }, + "node_modules/@speedy-tuner/ini/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@speedy-tuner/ini/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@speedy-tuner/types": { "version": "0.2.1", "resolved": "https://npm.pkg.github.com/download/@speedy-tuner/types/0.2.1/378faaf77fc78a8b33a9f09b9a742b272cc01a9349fe6fe61081e202d953210b", @@ -15733,6 +15761,11 @@ "node": ">= 0.8" } }, + "node_modules/parsimmon": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", + "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==" + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -27125,6 +27158,31 @@ "eslint-plugin-prettier": "^4.0.0" } }, + "@speedy-tuner/ini": { + "version": "0.2.2", + "resolved": "https://npm.pkg.github.com/download/@speedy-tuner/ini/0.2.2/38755d6ecbf8f478233f2a40251989192659620e0195f62eb6f4a79b920b9a0e", + "integrity": "sha512-sktFLjNF7oZa9haK+a71Ebc6csJ5uC/Huba879WiANydWRF7iIPdFyL6sfp8gOfoOj+I/jhg1MB+1z972hKdWw==", + "requires": { + "@speedy-tuner/types": "^0.2.1", + "js-yaml": "^4.1.0", + "parsimmon": "^1.18.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + } + } + }, "@speedy-tuner/types": { "version": "0.2.1", "resolved": "https://npm.pkg.github.com/download/@speedy-tuner/types/0.2.1/378faaf77fc78a8b33a9f09b9a742b272cc01a9349fe6fe61081e202d953210b", @@ -36304,6 +36362,11 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "parsimmon": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", + "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==" + }, "pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", diff --git a/package.json b/package.json index 167c187..95f907e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@reduxjs/toolkit": "^1.7.1", "@sentry/react": "^6.16.1", "@sentry/tracing": "^6.16.1", + "@speedy-tuner/ini": "^0.2.2", "@speedy-tuner/types": "^0.2.1", "antd": "^4.18.2", "easymde": "^2.15.0", diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts index ab1ac46..599822f 100644 --- a/src/hooks/useStorage.ts +++ b/src/hooks/useStorage.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import Storage from '../utils/storage'; +import Storage from '../utils/Storage'; const useStorage = () => { const storage = useMemo(() => new Storage(), []); diff --git a/src/pages/Diagnose.tsx b/src/pages/Diagnose.tsx index fe6dd07..452aa40 100644 --- a/src/pages/Diagnose.tsx +++ b/src/pages/Diagnose.tsx @@ -108,9 +108,12 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa setFileSize(formatBytes(compositeRaw.byteLength)); setStep(1); - const parser = new TriggerLogsParser(); - const resultComposite = parser.parse(pako.inflate(new Uint8Array(compositeRaw))).getCompositeLogs(); - const resultTooth = parser.parse(pako.inflate(new Uint8Array(toothRaw))).getToothLogs(); + const resultComposite = (new TriggerLogsParser(pako.inflate(new Uint8Array(compositeRaw)))) + .parse() + .getCompositeLogs(); + const resultTooth = (new TriggerLogsParser(pako.inflate(new Uint8Array(toothRaw)))) + .parse() + .getToothLogs(); setLogs(resultComposite); setToothLogs(resultTooth); diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx index 81dca04..db1ecd3 100644 --- a/src/pages/Upload.tsx +++ b/src/pages/Upload.tsx @@ -9,6 +9,7 @@ import { Divider, Input, notification, + Row, Skeleton, Space, Switch, @@ -25,6 +26,7 @@ import { ShareAltOutlined, FileTextOutlined, } from '@ant-design/icons'; +import { INI } from '@speedy-tuner/ini'; import { UploadRequestOption } from 'rc-upload/lib/interface'; import { UploadFile } from 'antd/lib/upload/interface'; import { useHistory } from 'react-router-dom'; @@ -55,6 +57,7 @@ import TuneParser from '../utils/tune/TuneParser'; import 'easymde/dist/easymde.min.css'; import TriggerLogsParser from '../utils/logs/TriggerLogsParser'; +import LogParser from '../utils/logs/LogParser'; enum MaxFiles { TUNE_FILES = 1, @@ -122,7 +125,7 @@ const UploadPage = () => { const [isPublic, setIsPublic] = useState(true); const [isListed, setIsListed] = useState(true); const [description, setDescription] = useState('# My Tune \ndescription'); - const [tuneFile, setTuneFile] = useState(null); + const [tuneFile, setTuneFile] = useState(null); const [logFiles, setLogFiles] = useState({}); const [toothLogFiles, setToothLogFiles] = useState({}); const [customIniFile, setCustomIniFile] = useState(null); @@ -166,12 +169,10 @@ const UploadPage = () => { } }; - const removeFile = (path: string) => { + const removeFile = async (path: string) => { try { - return deleteObject(storageRef(storage, path)); + return await deleteObject(storageRef(storage, path)); } catch (error) { - console.error(error); - genericError(error as Error); return Promise.reject(error); } }; @@ -278,17 +279,24 @@ const UploadPage = () => { const { path } = (options.data as unknown as UploadFileData); const tune: UploadedFile = {}; tune[(options.file as UploadFile).uid] = path; - setTuneFile(tune); upload(path, options, () => { updateDbData(newTuneId!, { tuneFile: path }); }, async (file) => { const { result, message } = await validateSize(file); if (!result) { + setTuneFile(false); return { result, message }; } + const valid = (new TuneParser()).parse(await file.arrayBuffer()).isValid(); + if (!valid) { + setTuneFile(false); + } else { + setTuneFile(tune); + } + return { - result: (new TuneParser()).parse(await file.arrayBuffer()).isValid(), + result: valid, message: 'Tune file is not valid!', }; }); @@ -297,14 +305,42 @@ const UploadPage = () => { const uploadLogs = async (options: UploadRequestOption) => { const { path } = (options.data as unknown as UploadFileData); const tune: UploadedFile = {}; - tune[(options.file as UploadFile).uid] = path; + const uuid = (options.file as UploadFile).uid; + tune[uuid] = path; const newValues = { ...logFiles, ...tune }; - setLogFiles(newValues); upload(path, options, () => { updateDbData(newTuneId!, { logFiles: Object.values(newValues) }); }, async (file) => { const { result, message } = await validateSize(file); - return { result, message }; + if (!result) { + return { result, message }; + } + + let valid = true; + const extension = file.name.split('.').pop(); + const parser = new LogParser(await file.arrayBuffer()); + + switch (extension) { + case 'mlg': + valid = parser.isMLG(); + break; + case 'msl': + case 'csv': + valid = parser.isMSL(); + break; + default: + valid = false; + break; + } + + if (valid) { + setLogFiles(newValues); + } + + return { + result: valid, + message: 'Log file is empty or not valid!', + }; }); }; @@ -313,7 +349,6 @@ const UploadPage = () => { const tune: UploadedFile = {}; tune[(options.file as UploadFile).uid] = path; const newValues = { ...toothLogFiles, ...tune }; - setToothLogFiles(newValues); upload(path, options, () => { updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) }); }, async (file) => { @@ -322,10 +357,15 @@ const UploadPage = () => { return { result, message }; } - const parser = (new TriggerLogsParser()).parse(await file.arrayBuffer()); + const parser = new TriggerLogsParser(await file.arrayBuffer()); + const valid = parser.isComposite() || parser.isTooth(); + + if (valid) { + setToothLogFiles(newValues); + } return { - result: parser.isComposite() || parser.isTooth(), + result: valid, message: 'Tooth logs file is empty or not valid!', }; }); @@ -335,21 +375,42 @@ const UploadPage = () => { const { path } = (options.data as unknown as UploadFileData); const tune: UploadedFile = {}; tune[(options.file as UploadFile).uid] = path; - setCustomIniFile(tune); upload(path, options, () => { updateDbData(newTuneId!, { customIniFile: path }); - }, () => Promise.resolve({ result: true, message: '' })); + }, async (file) => { + const { result, message } = await validateSize(file); + if (!result) { + return { result, message }; + } + + // TODO: change to common interface, add some validation method + const parser = new INI((new TextDecoder()).decode(await file.arrayBuffer())); + const valid = parser.parse().megaTune.signature.length > 0; + + if (valid) { + setCustomIniFile(tune); + } + + return { + result: valid, + message: 'INI file is empty or not valid!', + }; + }); }; const removeTuneFile = async (file: UploadFile) => { + if (tuneFile) { + removeFile(tuneFile[file.uid]); + } setTuneFile(null); - removeFile(tuneFile![file.uid]); updateDbData(newTuneId!, { tuneFile: null }); }; const removeLogFile = async (file: UploadFile) => { const { uid } = file; - removeFile(logFiles[file.uid]); + if (logFiles[file.uid]) { + removeFile(logFiles[file.uid]); + } const newValues = { ...logFiles }; delete newValues[uid]; setLogFiles(newValues); @@ -358,7 +419,9 @@ const UploadPage = () => { const removeToothLogFile = async (file: UploadFile) => { const { uid } = file; - removeFile(toothLogFiles[file.uid]); + if (toothLogFiles[file.uid]) { + removeFile(toothLogFiles[file.uid]); + } const newValues = { ...toothLogFiles }; delete newValues[uid]; setToothLogFiles(newValues); @@ -366,7 +429,9 @@ const UploadPage = () => { }; const removeCustomIniFile = async (file: UploadFile) => { - removeFile(customIniFile![file.uid]); + if (customIniFile) { + removeFile(customIniFile![file.uid]); + } setCustomIniFile(null); updateDbData(newTuneId!, { customIniFile: null }); }; @@ -415,29 +480,33 @@ const UploadPage = () => { const shareSection = ( <> Publish & Share - - - + {hasNavigatorShare && ( + + + ); @@ -562,7 +631,7 @@ const UploadPage = () => { disabled={isPublished} accept=".msq" > - {!tuneFile && uploadButton} + {tuneFile === null && uploadButton} {tuneFile && optionalSection} diff --git a/src/utils/ParserInterface.ts b/src/utils/ParserInterface.ts new file mode 100644 index 0000000..b62ff89 --- /dev/null +++ b/src/utils/ParserInterface.ts @@ -0,0 +1,7 @@ +export interface ParserInterface { + parse(): this; +} + +export interface ParserConstructor { + new(buffer: ArrayBuffer): ParserInterface; +} diff --git a/src/utils/storage.ts b/src/utils/Storage.ts similarity index 80% rename from src/utils/storage.ts rename to src/utils/Storage.ts index 629858d..5c21d30 100644 --- a/src/utils/storage.ts +++ b/src/utils/Storage.ts @@ -1,5 +1,5 @@ -import BrowserStorage from './storage/browserStorage'; -import { StorageInterface } from './storageInterface'; +import BrowserStorage from './storage/BrowserStorage'; +import { StorageInterface } from './StorageInterface'; class Storage { private storage: StorageInterface; diff --git a/src/utils/storageInterface.ts b/src/utils/StorageInterface.ts similarity index 100% rename from src/utils/storageInterface.ts rename to src/utils/StorageInterface.ts diff --git a/src/utils/logs/LogParser.ts b/src/utils/logs/LogParser.ts new file mode 100644 index 0000000..ac4f335 --- /dev/null +++ b/src/utils/logs/LogParser.ts @@ -0,0 +1,56 @@ +import { ParserInterface } from '../ParserInterface'; + +class LogParser implements ParserInterface { + private MLG_FORMAT_LENGTH = 6; + + private isMLGLogs: boolean = false; + + private isMSLLogs: boolean = false; + + private buffer: ArrayBuffer = new ArrayBuffer(0); + + private raw: string = ''; + + constructor(buffer: ArrayBuffer) { + this.buffer = buffer; + this.raw = (new TextDecoder()).decode(buffer); + + this.checkMLG(); + this.checkMSL(); + } + + parse(): this { + return this; + } + + isMLG(): boolean { + return this.isMLGLogs; + } + + isMSL(): boolean { + return this.isMSLLogs; + } + + private checkMLG() { + const fileFormat = new TextDecoder('utf8') + .decode(this.buffer.slice(0, this.MLG_FORMAT_LENGTH)) + // eslint-disable-next-line no-control-regex + .replace(/\x00/gu, ''); + + if (fileFormat === 'MLVLG') { + this.isMLGLogs = true; + } + } + + private checkMSL() { + const lines = this.raw.split('\n'); + for (let index = 0; index < lines.length; index++) { + if (lines[index].startsWith('Time')) { + this.isMSLLogs = true; + break; + } + } + } +} + +export default LogParser; diff --git a/src/utils/logs/TriggerLogsParser.ts b/src/utils/logs/TriggerLogsParser.ts index 08c9b85..f7905ee 100644 --- a/src/utils/logs/TriggerLogsParser.ts +++ b/src/utils/logs/TriggerLogsParser.ts @@ -1,3 +1,4 @@ +import { ParserInterface } from '../ParserInterface'; import { isNumber } from '../tune/expression'; export enum EntryType { @@ -23,7 +24,7 @@ export interface ToothLogEntry { time: number; } -class TriggerLogsParser { +class TriggerLogsParser implements ParserInterface { private COMMENT_PREFIX = '#'; private MARKER_PREFIX = 'MARK'; @@ -36,10 +37,17 @@ class TriggerLogsParser { private resultTooth: ToothLogEntry[] = []; - parse(buffer: ArrayBuffer): TriggerLogsParser { - const raw = (new TextDecoder()).decode(buffer); - this.parseCompositeLogs(raw); - this.parseToothLogs(raw); + private alreadyParsed: boolean = false; + + private raw: string = ''; + + constructor(buffer: ArrayBuffer) { + this.raw = (new TextDecoder()).decode(buffer); + } + + parse(): this { + this.parseCompositeLogs(this.raw); + this.parseToothLogs(this.raw); if (this.resultComposite.length > 0) { this.isCompositeLogs = true; @@ -49,6 +57,8 @@ class TriggerLogsParser { this.isToothLogs = true; } + this.alreadyParsed = true; + return this; } @@ -61,10 +71,18 @@ class TriggerLogsParser { } isTooth(): boolean { + if (!this.alreadyParsed) { + this.parse(); + } + return this.isToothLogs; } isComposite(): boolean { + if (!this.alreadyParsed) { + this.parse(); + } + return this.isCompositeLogs; } diff --git a/src/utils/storage/browserStorage.ts b/src/utils/storage/BrowserStorage.ts similarity index 92% rename from src/utils/storage/browserStorage.ts rename to src/utils/storage/BrowserStorage.ts index d1333cd..27cdfc1 100644 --- a/src/utils/storage/browserStorage.ts +++ b/src/utils/storage/BrowserStorage.ts @@ -1,4 +1,4 @@ -import { StorageInterface } from '../storageInterface'; +import { StorageInterface } from '../StorageInterface'; class BrowserStorage implements StorageInterface { private storage: Storage;