git-documentdb
Version:
Offline-first database that syncs with Git
307 lines • 12.6 kB
JavaScript
;
/**
* GitDocumentDB
* Copyright (c) Hidekazu Kubota
*
* This source code is licensed under the Mozilla Public License Version 2.0
* found in the LICENSE file in the root directory of this source tree.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Validator = void 0;
const path_1 = __importDefault(require("path"));
const cross_blob_1 = __importDefault(require("cross-blob"));
const const_1 = require("./const");
const error_1 = require("./error");
/**
* Validator Class
*
* @public
*/
class Validator {
constructor(workingDir) {
this._workingDirectory = workingDir;
}
/**
* Normalized collectionPath is '' or path strings that have a trailing slash and no heading slash.
* Root ('/') is not allowed.
* Backslash \\ or yen ¥ is replaced with slash /.
*/
static normalizeCollectionPath(collectionPath) {
if (collectionPath === undefined || collectionPath === '') {
return '';
}
collectionPath = collectionPath.replace(/\\/g, '/');
collectionPath = collectionPath.replace(/¥/g, '/');
// Integrate consecutive slash
collectionPath = collectionPath.replace(/\/+/g, '/');
if (collectionPath === '/') {
return '';
}
// Remove heading slash
if (collectionPath.startsWith('/')) {
collectionPath = collectionPath.slice(1);
}
// Set only one trailing slash
if (!collectionPath.endsWith('/')) {
collectionPath += '/';
}
return collectionPath;
}
/**
* Return the max length of working directory path
*/
static maxWorkingDirectoryLength() {
// Trailing slash of workingDirectory is omitted.
// Full path is `${_workingDirectory}/a.json`
// Don't care about '.md' for simplicity.
const minimumMemberLength = 7; // '/a.json'
return const_1.MAX_FILE_PATH_LENGTH - minimumMemberLength;
}
/**
* Return the max length of collectionPath
*/
maxCollectionPathLength() {
// Suppose that collectionPath is normalized.
// Trailing slash of workingDirectory is omitted.
// Full path is `${_workingDirectory}/${collectionPath}${shortId}.json`
// Don't care about '.md' for simplicity.
const minIdLength = 6; // 'a.json'
return const_1.MAX_FILE_PATH_LENGTH - this._workingDirectory.length - 1 - minIdLength;
}
/**
* Return the max length of _id
*
* @remarks
* _id means `${collectionPath}/${shortId}`
*/
maxIdLength() {
// Suppose that collectionPath is normalized.
// Trailing slash of workingDirectory is omitted.
// Full path is `${_workingDirectory}/${collectionPath}${shortId}.json`
// Don't care about '.md' for simplicity.
const extLength = 5; // '.json'
return const_1.MAX_FILE_PATH_LENGTH - this._workingDirectory.length - 1 - extLength;
}
/**
* Return false if the given name equals Windows reserved filename
*/
testWindowsReservedFileName(name, options) {
var _a;
options !== null && options !== void 0 ? options : (options = {
allowDirectoryDot: undefined,
});
(_a = options.allowDirectoryDot) !== null && _a !== void 0 ? _a : (options.allowDirectoryDot = false);
if (name.match(/^(CON|PRN|AUX|NUL|COM1|COM2|COM3|COM4|COM5|COM6|COM7|COM8|COM9|LPT1|LPT2|LPT3|LPT4|LPT5|LPT6|LPT7|LPT8|LPT9)$/)) {
return false;
}
if (!options.allowDirectoryDot && (name === '.' || name === '..')) {
return false;
}
return true;
}
/**
* Return false if the given name includes Windows invalid filename character
*/
// eslint-disable-next-line complexity
testWindowsInvalidFileNameCharacter(name, options) {
var _a, _b, _c, _d, _e;
options !== null && options !== void 0 ? options : (options = {
allowSlash: undefined,
allowDriveLetter: undefined,
allowDirectoryDot: undefined,
allowDot: undefined,
allowLastSpace: undefined,
});
(_a = options.allowSlash) !== null && _a !== void 0 ? _a : (options.allowSlash = false);
(_b = options.allowDriveLetter) !== null && _b !== void 0 ? _b : (options.allowDriveLetter = false);
(_c = options.allowDirectoryDot) !== null && _c !== void 0 ? _c : (options.allowDirectoryDot = false);
(_d = options.allowDot) !== null && _d !== void 0 ? _d : (options.allowDot = false);
(_e = options.allowLastSpace) !== null && _e !== void 0 ? _e : (options.allowLastSpace = false);
let regStr = `<>:"|?*\0`;
if (!options.allowSlash) {
regStr += `/`;
regStr += `\\\\`;
regStr += `¥`;
}
const regExp = new RegExp(`[${regStr}]`);
if (options.allowDriveLetter) {
name = name.replace(/^[A-Za-z]:/, '');
}
if (name.match(regExp)) {
return false;
}
// Do not end with space or period.
if (options.allowDot) {
// nop
}
else if (options.allowDirectoryDot) {
if (name !== '.' && name !== '..' && name.endsWith('.')) {
return false;
}
}
else if (name.endsWith('.')) {
return false;
}
if (!options.allowLastSpace && name.endsWith(' ')) {
return false;
}
return true;
}
/**
* Validate localDir
*
* @remarks
*```
* - A directory name allows Unicode characters except for OS reserved filenames and the following characters: \< \> : " | ? * \\0
* - A colon is generally disallowed, but a drive letter followed by a colon is allowed.
* - A directory name cannot end with a period or a white space but the current directory . and the parent directory .. are allowed.
* - A trailing slash could be omitted.
*```
* @throws {@link Err.InvalidLocalDirCharacterError}
*/
validateLocalDir(localDir) {
localDir = localDir.replace(/\\/g, '/');
localDir = localDir.replace(/¥/g, '/');
// Integrate consecutive slash
localDir = localDir.replace(/\/+/g, '/');
// Remove heading and trailing slash
if (localDir.startsWith('/')) {
localDir = localDir.slice(1);
}
if (localDir.endsWith('/')) {
localDir = localDir.slice(0, -1);
}
// localDir is formatted like 'a/b/c'
const arr = localDir.split('/');
arr.forEach(part => {
// allowDirectoryDot
// '/./a/b/c','a/b/c/.', 'a/b/c/./' are all valid.
if (!this.testWindowsReservedFileName(part, { allowDirectoryDot: true }) ||
!this.testWindowsInvalidFileNameCharacter(part, {
allowDriveLetter: true,
allowDirectoryDot: true,
})) {
throw new error_1.Err.InvalidLocalDirCharacterError(part);
}
});
}
/**
* Validate dbName
*
* @remarks
*```
* - dbName allows Unicode characters except for OS reserved filenames and the following characters: \< \> : " ¥ / \\ | ? * \\0
* - dbName cannot end with a period or a white space.
* - dbName does not allow '.' and '..'.
*```
* @throws {@link Err.InvalidDbNameCharacterError}
*/
validateDbName(dbName) {
if (!this.testWindowsReservedFileName(dbName) ||
!this.testWindowsInvalidFileNameCharacter(dbName)) {
throw new error_1.Err.InvalidDbNameCharacterError(dbName);
}
}
/**
* Validate collectionPath
*
* @remarks CollectionPath must be NULL string or paths that match the following conditions:
*```
* - CollectionPath can include paths separated by slashes.
* - A directory name in paths allows Unicode characters except for OS reserved filenames and the following characters: \< \> : " | ? * \\0
* - **It is recommended to use ASCII characters and case-insensitive names for cross-platform.**
* - A directory name in paths cannot end with a period or a white space.
* - A directory name in paths does not allow '.' and '..'.
* - CollectionPath cannot start with a slash.
* - Trailing slash could be omitted. e.g.) 'pages' and 'pages/' show the same CollectionPath.
*```
*
* @throws {@link Err.InvalidCollectionPathCharacterError}
* @throws {@link Err.InvalidCollectionPathLengthError}
*/
validateCollectionPath(collectionPath) {
if (collectionPath === '') {
return;
}
if (collectionPath.startsWith('/')) {
throw new error_1.Err.InvalidCollectionPathCharacterError('/');
}
const normalized = Validator.normalizeCollectionPath(collectionPath);
const trailingSlashRemoved = normalized.slice(0, -1);
const arr = trailingSlashRemoved.split('/');
arr.forEach(part => {
if (!this.testWindowsReservedFileName(part) ||
!this.testWindowsInvalidFileNameCharacter(part)) {
throw new error_1.Err.InvalidCollectionPathCharacterError(part);
}
});
const minimumCollectionPathLength = 0; // minimum is ''
if (Validator.byteLengthOf(normalized) < minimumCollectionPathLength ||
Validator.byteLengthOf(normalized) > this.maxCollectionPathLength()) {
throw new error_1.Err.InvalidCollectionPathLengthError(normalized, minimumCollectionPathLength, this.maxCollectionPathLength());
}
}
/**
* Validate _id
*
* _id = collectionPath + shortId (not including postfix '.json')
*
* @remarks Spec of _id is described at {@link JsonDoc}.
* @throws {@link Err.InvalidIdCharacterError}
* @throws {@link Err.InvalidCollectionPathCharacterError}
* @throws {@link Err.InvalidCollectionPathLengthError}
* @throws {@link Err.InvalidIdLengthError}
*/
validateId(_id) {
const baseName = path_1.default.basename(_id);
// basename returns '' if _id is '/'.
// basename also returns '' if _id is '\' only on Windows
if (baseName === '') {
throw new error_1.Err.InvalidIdCharacterError(_id);
}
// basename returns a last directory name if _id ends with a slash.
// e.g.) basename('/users/pages/') returns 'pages'.
if (_id.endsWith('/')) {
throw new error_1.Err.InvalidIdCharacterError(_id);
}
if (!this.testWindowsInvalidFileNameCharacter(baseName, {
allowDot: true,
allowLastSpace: true,
})) {
throw new error_1.Err.InvalidIdCharacterError(_id);
}
const dirName = path_1.default.dirname(_id);
// dirname returns '.' if _id does not include slashes.
// dirname also returns '.' if _id is './xxx'
if (dirName !== '.' || _id.startsWith('./')) {
this.validateCollectionPath(path_1.default.dirname(_id));
}
// Example of a minimum _id is 'a'
const minimumIdLength = 1;
if (Validator.byteLengthOf(_id) < minimumIdLength ||
Validator.byteLengthOf(_id) > this.maxIdLength()) {
throw new error_1.Err.InvalidIdLengthError(_id, minimumIdLength, this.maxIdLength());
}
}
/**
* Validate document
*
* @throws {@link Err.UndefinedDocumentIdError}
* @throws {@link Err.InvalidIdCharacterError}
* @throws {@link Err.InvalidIdLengthError}
*/
validateDocument(doc) {
if (doc._id === undefined) {
throw new error_1.Err.UndefinedDocumentIdError();
}
this.validateId(doc._id);
}
}
exports.Validator = Validator;
Validator.byteLengthOf = (str) => {
return new cross_blob_1.default([str], { type: 'text/plain' }).size;
};
//# sourceMappingURL=validator.js.map