import path from 'path';
import fs from 'fs';
-function pathnameToNativePathname(root, pathname) {
- // pathname: [''], ['', <name>], ['', <name>, <name>], ...
- // <name> !== ''
- // <name> !== '.'
- // <name> !== '..'
- return path.join(root, ...pathname);
+function checkIsWritable(options) {
+ if (!options.writable) {
+ throw new Error('The file system is not writable.');
+ }
}
-function checkIsDirectory(nativePathname) {
- const stat = fs.statSync(nativePathname, {throwIfNoEntry: false});
- if (stat === undefined) {
- throw new Error('The file does not exist.');
- }
- if (!stat.isDirectory()) {
- throw new Error('The file is not a directory.');
- }
+function toNativePathname(pathnameSegments, options) {
+ return path.join(options.root, ...pathnameSegments);
}
-function checkIsNotDirectory(nativePathname) {
+function checkIsRegularFile(nativePathname) {
const stat = fs.statSync(nativePathname, {throwIfNoEntry: false});
if (stat === undefined) {
throw new Error('The file does not exist.');
}
- if (stat.isDirectory()) {
- throw new Error('The file is a directory.');
+ if (!stat.isFile()) {
+ throw new Error('The file is not a regular file.');
}
}
return {writable: options.writable};
}
-export function getFileContents(pathname, options) {
- const nativePathname = pathnameToNativePathname(options.root, pathname);
- checkIsNotDirectory(nativePathname);
+export function getFileContents(pathnameSegments, options) {
+ const nativePathname = toNativePathname(pathnameSegments, options);
+ checkIsRegularFile(nativePathname);
return fs.readFileSync(nativePathname);
}
-export function putFileContents(pathname, contents, options) {
- if (!options.writable) {
- throw new Error('The file system is not writable.');
- }
- const nativePathname = pathnameToNativePathname(options.root, pathname);
- checkIsNotDirectory(nativePathname);
+export function putFileContents(pathnameSegments, contents, options) {
+ checkIsWritable(options);
+ const nativePathname = toNativePathname(pathnameSegments, options);
+ checkIsRegularFile(nativePathname);
fs.writeFileSync(nativePathname, contents);
}
// SPDX-License-Identifier: BSD-3-Clause
import express from 'express';
+import fs from 'fs';
+import { Buffer } from 'buffer';
class ClientError extends Error {}
-function isString(value) {
- return typeof value === 'string';
+function invalidPathname() {
+ throw new Error('Invalid pathname.');
}
-function invalidStringParameter() {
- throw new Error('Invalid string parameter.');
-}
-
-function validateStringParameter(value) {
- if (!isString(value)) {
- invalidStringParameter();
+function getPathnameSegments(req) {
+ const pathname = req.query.pathname;
+ if (typeof pathname !== 'string') {
+ invalidPathname();
}
- return value;
-}
-
-function invalidNonNegativeIntegerParameter() {
- throw new Error('Invalid non-negative integer parameter.');
-}
-
-function validateNonNegativeIntegerParameter(value) {
- if (!isString(value) || !/^(0|[1-9][0-9]*)$/.test(value)) {
- invalidNonNegativeIntegerParameter();
+ const pathnameSegments = pathname.split('/');
+ if (pathnameSegments.length === 1) {
+ invalidPathname();
}
- return Number.parseInt(value);
-}
-
-function invalidBooleanParameter() {
- throw new Error('Invalid boolean parameter.');
-}
-
-function validateBooleanParameter(value) {
- if (!isString(value)) {
- invalidBooleanParameter();
+ if (pathnameSegments[0] !== '') {
+ invalidPathname();
}
- switch (value) {
- case 'true':
- return true;
- case 'false':
- return false;
- default:
- invalidBooleanParameter();
+ for (let i = 1; i < pathnameSegments.length; i++) {
+ if (pathnameSegments[i] === '..') {
+ invalidPathname();
+ }
}
+ return pathnameSegments.slice(1);
}
-function isValidName(name) {
- return !name.includes('\x00') &&
- !name.includes('/') &&
- name !== '' &&
- name !== '.' &&
- name !== '..';
+function invalidContents() {
+ throw new Error('Invalid contents.');
}
-function invalidNameParameter() {
- throw new Error('Invalid name parameter.');
-}
-
-function validateNameParameter(name) {
- if (!isString(name) || !isValidName(name)) {
- invalidNameParameter();
+function getContents(req) {
+ const contents = req.body;
+ if (!Buffer.isBuffer(contents)) {
+ invalidContents();
}
- return name;
-}
-
-function invalidNamesParameter() {
- throw new Error('Invalid names parameter.');
+ return contents;
}
-function validateNamesParameter(names) {
- if (!Array.isArray(names)) {
- invalidNamesParameter();
- }
- if (names.length === 0) {
- invalidNamesParameter();
- }
- for (const name of names) {
- if (!isString(name) || !isValidName(name)) {
- invalidNamesParameter();
+function makeRouter(fileSystem, options) {
+ let root = null;
+ try {
+ root = fs.realpathSync.native(options.root);
+ } catch (exception) {
+ if (exception.code = 'ENOENT') {
+ throw new Error('The root does not exist.');
+ } else {
+ throw exception;
}
}
- if (new Set(names).size !== names.length) {
- invalidNamesParameter();
+ const stat = fs.statSync(root);
+ if (!stat.isDirectory()) {
+ throw new Error('The root is not a directory.');
}
- names.sort();
- return names;
-}
-
-function invalidPathnameParameter() {
- throw new Error('Invalid pathname parameter.');
-}
-
-function validatePathnameParameter(pathname) {
- if (!isString(pathname)) {
- invalidPathnameParameter();
- }
- const names = pathname.split('/');
- if (names[0] !== '') {
- invalidPathnameParameter();
- }
- for (let i = 1; i < names.length; i++) {
- if (!isValidName(names[i])) {
- invalidPathnameParameter();
- }
- }
- return names;
-}
-
-function makeRouter(fileSystem, options) {
+ options.root = root;
options.ClientError = ClientError;
const router = express.Router();
router.get('/get-capabilities', (req, res) => {
res.json(fileSystem.getCapabilities(options));
});
router.get('/get-file-contents', (req, res) => {
- res.send(fileSystem.getFileContents(validatePathnameParameter(req.query.pathname), options));
+ const pathnameSegments = getPathnameSegments(req);
+ res.send(fileSystem.getFileContents(pathnameSegments, options));
});
- router.put('/put-file-contents', express.raw({limit: '250kb'}), (req, res) => {
- fileSystem.putFileContents(validatePathnameParameter(req.query.pathname), req.body, options);
+ router.put('/put-file-contents', express.raw({limit: '1mb'}), (req, res) => {
+ const pathnameSegments = getPathnameSegments(req);
+ const contents = getContents(req);
+ fileSystem.putFileContents(pathnameSegments, contents, options);
res.end();
});
router.use((err, req, res, next) => {