diff --git a/CHANGELOG.md b/CHANGELOG.md index bc90041f..1a267ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Version 2.0.0 - The masterIP and masterPort config entries has been merged into masterURL. Breaks discordChat, playerManager, serverSelect, trainTeleports and clusterioModel. +- Instance names can no longer be invalid Windows file names. Version 1.2.1 diff --git a/client.js b/client.js index ae8dd34e..c8887503 100644 --- a/client.js +++ b/client.js @@ -63,11 +63,55 @@ class Instance { } } +function checkFilename(name) { + // All of these are bad in Windows only, except for /, . and .. + // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; + const badEnd = /[. ]$/; + + const oneToNine = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + const badNames = [ + // Relative path components + '.', '..', + + // Reserved filenames in Windows + 'CON', 'PRN', 'AUX', 'NUL', + ...oneToNine.map(n => `COM${n}`), + ...oneToNine.map(n => `LPT${n}`), + ]; + + if (typeof name !== "string") { + throw new Error("must be a string"); + } + + if (name === "") { + throw new Error("cannot be empty"); + } + + if (badChars.test(name)) { + throw new Error('cannot contain <>:"\\/|=* or control characters'); + } + + if (badNames.includes(name.toUpperCase())) { + throw new Error( + "cannot be named any of . .. CON PRN AUX NUL COM1-9 and LPT1-9" + ); + } + + if (badEnd.test(name)) { + throw new Error("cannot end with . or space"); + } +} let instance; if (process.argv[3] !== undefined) { let name = process.argv[3]; + try { + checkFilename(name); + } catch (err) { + throw new Error(`Instance name ${err.message}`); + } let dir = path.join(config.instanceDirectory, name); instance = new Instance(dir, name); } @@ -892,6 +936,7 @@ function hashMods(instance, callback) { module.exports = { // For testing only _Instance: Instance, + _checkFilename: checkFilename, _randomDynamicPort: randomDynamicPort, _generatePassword: generatePassword, }; diff --git a/test/client.js b/test/client.js index 84c0e593..0260174d 100644 --- a/test/client.js +++ b/test/client.js @@ -19,6 +19,58 @@ describe("Client testing", function() { }) }); + describe("checkFilename()", function() { + it("should allow a basic name", function() { + client._checkFilename("file"); + }); + + function check(item, msg) { + assert.throws(() => client._checkFilename(item), new Error(msg)); + } + + it("should throw on non-string", function() { + check(undefined, "must be a string"); + check(null, "must be a string"); + check({}, "must be a string"); + check([], "must be a string"); + check(0, "must be a string"); + check(false, "must be a string"); + }); + + it("should throw on empty name", function() { + check("", "cannot be empty"); + }); + + it("should throw on <>:\"\\/|?* \\x00\\r\\n\\t", function() { + for (let char of '<>:"\\/|?*\x00\r\n\t') { + check(char, 'cannot contain <>:"\\/|=* or control characters'); + } + }); + + it("should throw on CON, PRN, AUX, NUL, COM1, LPT1", function() { + for (let bad of ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1']) { + check( + bad, 'cannot be named any of . ..' + +' CON PRN AUX NUL COM1-9 and LPT1-9' + ); + } + }); + + it("should throw on . and ..", function() { + for (let bad of ['.', '..']) { + check( + bad, 'cannot be named any of . ..' + +' CON PRN AUX NUL COM1-9 and LPT1-9' + ); + } + }); + + it("should throw on names ending with . or space", function() { + check("a ", "cannot end with . or space"); + check("a.", "cannot end with . or space"); + }); + }); + describe("randomDynamicPort()", function() { it("should return a port number", function() { let port = client._randomDynamicPort()