diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts index ebb50b19b2..fdbc48c08f 100644 --- a/src/init/CliRunner.ts +++ b/src/init/CliRunner.ts @@ -1,12 +1,11 @@ /* eslint-disable unicorn/no-process-exit */ -import * as path from 'path'; import type { ReadStream, WriteStream } from 'tty'; import type { LoaderProperties } from 'componentsjs'; import { Loader } from 'componentsjs'; import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; -import { ensureTrailingSlash } from '../util/PathUtil'; +import { joinFilePath, toSystemFilePath, ensureTrailingSlash } from '../util/PathUtil'; import type { Initializer } from './Initializer'; export class CliRunner { @@ -44,14 +43,14 @@ export class CliRunner { // Gather settings for instantiating the server const loaderProperties: LoaderProperties = { - mainModulePath: this.resolvePath(params.mainModulePath), + mainModulePath: toSystemFilePath(this.resolveFilePath(params.mainModulePath)), scanGlobal: params.globalModules, }; - const configFile = this.resolvePath(params.config, 'config/config-default.json'); + const configFile = this.resolveFilePath(params.config, 'config/config-default.json'); const variables = this.createVariables(params); // Create and execute the server initializer - this.createInitializer(loaderProperties, configFile, variables) + this.createInitializer(loaderProperties, toSystemFilePath(configFile), variables) .then( async(initializer): Promise => initializer.handleSafe(), (error: Error): void => { @@ -70,10 +69,10 @@ export class CliRunner { * Resolves a path relative to the current working directory, * falling back to a path relative to this module. */ - protected resolvePath(cwdPath?: string | null, modulePath = ''): string { + protected resolveFilePath(cwdPath?: string | null, modulePath = ''): string { return typeof cwdPath === 'string' ? - path.join(process.cwd(), cwdPath) : - path.join(__dirname, '../../', modulePath); + joinFilePath(process.cwd(), cwdPath) : + joinFilePath(__dirname, '../../', modulePath); } /** @@ -85,10 +84,11 @@ export class CliRunner { params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`, 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, 'urn:solid-server:default:variable:port': params.port, - 'urn:solid-server:default:variable:rootFilePath': this.resolvePath(params.rootFilePath), + 'urn:solid-server:default:variable:rootFilePath': + this.resolveFilePath(params.rootFilePath), 'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint, 'urn:solid-server:default:variable:podTemplateFolder': - params.podTemplateFolder ?? this.resolvePath(null, 'templates'), + this.resolveFilePath(params.podTemplateFolder, 'templates'), }; } diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts index a35cfefc66..f7982cf5c3 100644 --- a/src/pods/generate/TemplatedResourcesGenerator.ts +++ b/src/pods/generate/TemplatedResourcesGenerator.ts @@ -1,5 +1,4 @@ import { promises as fsPromises } from 'fs'; -import { posix } from 'path'; import { Parser } from 'n3'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; @@ -8,14 +7,12 @@ import type { FileIdentifierMapperFactory, ResourceLink, } from '../../storage/mapping/FileIdentifierMapper'; -import { isContainerIdentifier } from '../../util/PathUtil'; +import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; import { guardedStreamFrom } from '../../util/StreamUtil'; import type { Resource, ResourcesGenerator } from './ResourcesGenerator'; import type { TemplateEngine } from './TemplateEngine'; import Dict = NodeJS.Dict; -const { join: joinPath } = posix; - /** * Generates resources by making use of a template engine. * The template folder structure will be kept. @@ -77,7 +74,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator { private async* generateLinks(folderPath: string, mapper: FileIdentifierMapper): AsyncIterable { const files = await fsPromises.readdir(folderPath); for (const name of files) { - const filePath = joinPath(folderPath, name); + const filePath = joinFilePath(folderPath, name); const stats = await fsPromises.lstat(filePath); yield mapper.mapFilePathToUrl(filePath, stats.isDirectory()); } diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts index ca832f986b..73fee26579 100644 --- a/src/storage/accessors/FileDataAccessor.ts +++ b/src/storage/accessors/FileDataAccessor.ts @@ -1,6 +1,5 @@ import type { Stats } from 'fs'; import { createWriteStream, createReadStream, promises as fsPromises } from 'fs'; -import { posix } from 'path'; import type { Readable } from 'stream'; import { DataFactory } from 'n3'; import type { NamedNode, Quad } from 'rdf-js'; @@ -13,7 +12,7 @@ import { isSystemError } from '../../util/errors/SystemError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; import { guardStream } from '../../util/GuardedStream'; import type { Guarded } from '../../util/GuardedStream'; -import { isContainerIdentifier } from '../../util/PathUtil'; +import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil'; import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import { toLiteral } from '../../util/TermUtil'; @@ -21,8 +20,6 @@ import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies' import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper'; import type { DataAccessor } from './DataAccessor'; -const { join: joinPath } = posix; - /** * DataAccessor that uses the file system to store documents as files and containers as folders. */ @@ -298,14 +295,14 @@ export class FileDataAccessor implements DataAccessor { } // Ignore non-file/directory entries in the folder - const childStats = await fsPromises.lstat(joinPath(link.filePath, childName)); + const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName)); if (!childStats.isFile() && !childStats.isDirectory()) { continue; } // Generate the URI corresponding to the child resource const childLink = await this.resourceMapper - .mapFilePathToUrl(joinPath(link.filePath, childName), childStats.isDirectory()); + .mapFilePathToUrl(joinFilePath(link.filePath, childName), childStats.isDirectory()); // Generate metadata of this specific child const subject = DataFactory.namedNode(childLink.identifier.path); diff --git a/src/storage/mapping/ExtensionBasedMapper.ts b/src/storage/mapping/ExtensionBasedMapper.ts index f2a9982d51..9485876667 100644 --- a/src/storage/mapping/ExtensionBasedMapper.ts +++ b/src/storage/mapping/ExtensionBasedMapper.ts @@ -1,5 +1,4 @@ import { promises as fsPromises } from 'fs'; -import { posix } from 'path'; import * as mime from 'mime-types'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../../logging/LogUtil'; @@ -9,13 +8,13 @@ import { encodeUriPathComponents, ensureTrailingSlash, isContainerIdentifier, + joinFilePath, + normalizeFilePath, trimTrailingSlashes, } from '../../util/PathUtil'; import type { FileIdentifierMapper, FileIdentifierMapperFactory, ResourceLink } from './FileIdentifierMapper'; import { getAbsolutePath, getRelativePath, validateRelativePath } from './MapperUtil'; -const { join: joinPath, normalize: normalizePath } = posix; - export interface ResourcePath { /** @@ -50,7 +49,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper { public constructor(base: string, rootFilepath: string, overrideTypes = { acl: TEXT_TURTLE, meta: TEXT_TURTLE }) { this.baseRequestURI = trimTrailingSlashes(base); - this.rootFilepath = trimTrailingSlashes(normalizePath(rootFilepath)); + this.rootFilepath = trimTrailingSlashes(normalizeFilePath(rootFilepath)); this.types = { ...mime.types, ...overrideTypes }; } @@ -101,7 +100,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper { // Matching file found if (fileName) { - filePath = joinPath(folder, fileName); + filePath = joinFilePath(folder, fileName); } this.logger.info(`The path for ${identifier.path} is ${filePath}`); diff --git a/src/storage/mapping/FixedContentTypeMapper.ts b/src/storage/mapping/FixedContentTypeMapper.ts index 700c6e799d..855c9cdd09 100644 --- a/src/storage/mapping/FixedContentTypeMapper.ts +++ b/src/storage/mapping/FixedContentTypeMapper.ts @@ -1,17 +1,16 @@ -import { posix } from 'path'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../../logging/LogUtil'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { encodeUriPathComponents, - ensureTrailingSlash, isContainerIdentifier, + ensureTrailingSlash, + isContainerIdentifier, + normalizeFilePath, trimTrailingSlashes, } from '../../util/PathUtil'; import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper'; import { getAbsolutePath, getRelativePath, validateRelativePath } from './MapperUtil'; -const { normalize: normalizePath } = posix; - /** * A mapper that always returns a fixed content type for files. */ @@ -24,7 +23,7 @@ export class FixedContentTypeMapper implements FileIdentifierMapper { public constructor(base: string, rootFilepath: string, contentType: string) { this.baseRequestURI = trimTrailingSlashes(base); - this.rootFilepath = trimTrailingSlashes(normalizePath(rootFilepath)); + this.rootFilepath = trimTrailingSlashes(normalizeFilePath(rootFilepath)); this.contentType = contentType; } diff --git a/src/storage/mapping/MapperUtil.ts b/src/storage/mapping/MapperUtil.ts index 22f6b3a030..f68f005209 100644 --- a/src/storage/mapping/MapperUtil.ts +++ b/src/storage/mapping/MapperUtil.ts @@ -1,11 +1,8 @@ -import { posix } from 'path'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../../logging/LogUtil'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; -import { decodeUriPathComponents } from '../../util/PathUtil'; - -const { join: joinPath } = posix; +import { joinFilePath, decodeUriPathComponents } from '../../util/PathUtil'; const logger = getLoggerFor('MapperUtil'); @@ -18,7 +15,7 @@ const logger = getLoggerFor('MapperUtil'); * @returns Absolute path of the file. */ export const getAbsolutePath = (rootFilepath: string, path: string, identifier = ''): string => - joinPath(rootFilepath, path, identifier); + joinFilePath(rootFilepath, path, identifier); /** * Strips the baseRequestURI from the identifier and checks if the stripped base URI matches the store's one. diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts index 02992a32ef..a25966f166 100644 --- a/src/util/PathUtil.ts +++ b/src/util/PathUtil.ts @@ -1,5 +1,46 @@ +import platform, { posix } from 'path'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +/** + * Changes a potential Windows path into a POSIX path. + * + * @param path - Path to check (POSIX or Windows). + * + * @returns The potentially changed path (POSIX). + */ +const windowsToPosixPath = (path: string): string => path.replace(/\\+/gu, '/'); + +/** + * Resolves relative segments in the path/ + * + * @param path - Path to check (POSIX or Windows). + * + * @returns The potentially changed path (POSIX). + */ +export const normalizeFilePath = (path: string): string => + posix.normalize(windowsToPosixPath(path)); + +/** + * Adds the paths to the base path. + * + * @param basePath - The base path (POSIX or Windows). + * @param paths - Subpaths to attach (POSIX). + * + * @returns The potentially changed path (POSIX). + */ +export const joinFilePath = (basePath: string, ...paths: string[]): string => + posix.join(windowsToPosixPath(basePath), ...paths); + +/** + * Converts the path into an OS-dependent path. + * + * @param path - Path to check (POSIX). + * + * @returns The potentially changed path (OS-dependent). + */ +export const toSystemFilePath = (path: string): string => + platform.normalize(path); + /** * Makes sure the input path has exactly 1 slash at the end. * Multiple slashes will get merged into one. diff --git a/test/integration/Config.ts b/test/integration/Config.ts index 14e0361692..c486d1f283 100644 --- a/test/integration/Config.ts +++ b/test/integration/Config.ts @@ -1,8 +1,7 @@ import { mkdirSync } from 'fs'; -import { join } from 'path'; -import * as Path from 'path'; import { Loader } from 'componentsjs'; import * as rimraf from 'rimraf'; +import { joinFilePath, toSystemFilePath } from '../../src/util/PathUtil'; export const BASE = 'http://test.com'; @@ -12,17 +11,17 @@ export const BASE = 'http://test.com'; export const instantiateFromConfig = async(componentUrl: string, configFile: string, variables?: Record): Promise => { // Initialize the Components.js loader - const mainModulePath = Path.join(__dirname, '../../'); + const mainModulePath = joinFilePath(__dirname, '../../'); const loader = new Loader({ mainModulePath }); await loader.registerAvailableModuleResources(); // Instantiate the component from the config - const configPath = Path.join(__dirname, 'config', configFile); + const configPath = toSystemFilePath(joinFilePath(__dirname, 'config', configFile)); return loader.instantiateFromUrl(componentUrl, configPath, undefined, { variables }); }; export const getTestFolder = (name: string): string => - join(__dirname, '../tmp', name); + joinFilePath(__dirname, '../tmp', name); export const createFolder = (folder: string): void => { mkdirSync(folder, { recursive: true }); diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index 7dc57bcc21..2c716753b8 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -1,9 +1,9 @@ import { createReadStream } from 'fs'; -import { join } from 'path'; import type { HttpHandler, Initializer, ResourceStore } from '../../src/'; -import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; -import { guardStream } from '../../src/util/GuardedStream'; -import { CONTENT_TYPE, LDP } from '../../src/util/Vocabularies'; +import { + CONTENT_TYPE, LDP, + RepresentationMetadata, guardStream, joinFilePath, +} from '../../src/'; import { AclHelper, ResourceHelper } from '../util/TestHelpers'; import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config'; @@ -58,7 +58,7 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se // Write test resource await store.setRepresentation({ path: `${BASE}/permanent.txt` }, { binary: true, - data: guardStream(createReadStream(join(__dirname, '../assets/permanent.txt'))), + data: guardStream(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt'))), metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }), }); }); diff --git a/test/integration/PodCreation.test.ts b/test/integration/PodCreation.test.ts index 68951044fd..69461dd92d 100644 --- a/test/integration/PodCreation.test.ts +++ b/test/integration/PodCreation.test.ts @@ -1,7 +1,7 @@ import type { Server } from 'http'; -import { join } from 'path'; import fetch from 'cross-fetch'; import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import { joinFilePath } from '../../src/util/PathUtil'; import { readableToString } from '../../src/util/StreamUtil'; import { instantiateFromConfig } from './Config'; @@ -17,7 +17,7 @@ describe('A server with a pod handler', (): void => { 'urn:solid-server:default:ServerFactory', 'server-without-auth.json', { 'urn:solid-server:default:variable:port': port, 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:podTemplateFolder': join(__dirname, '../assets/templates'), + 'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../assets/templates'), }, ) as HttpServerFactory; server = factory.startServer(port); diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts index c414d94e1c..e024d6d542 100644 --- a/test/unit/init/CliRunner.test.ts +++ b/test/unit/init/CliRunner.test.ts @@ -1,7 +1,7 @@ -import * as path from 'path'; import { Loader } from 'componentsjs'; import { CliRunner } from '../../../src/init/CliRunner'; import type { Initializer } from '../../../src/init/Initializer'; +import { joinFilePath, toSystemFilePath } from '../../../src/util/PathUtil'; const initializer: jest.Mocked = { handleSafe: jest.fn(), @@ -34,12 +34,12 @@ describe('CliRunner', (): void => { expect(Loader).toHaveBeenCalledTimes(1); expect(Loader).toHaveBeenCalledWith({ - mainModulePath: path.join(__dirname, '../../../'), + mainModulePath: toSystemFilePath(joinFilePath(__dirname, '../../../')), }); expect(loader.instantiateFromUrl).toHaveBeenCalledTimes(1); expect(loader.instantiateFromUrl).toHaveBeenCalledWith( 'urn:solid-server:default:Initializer', - path.join(__dirname, '/../../../config/config-default.json'), + toSystemFilePath(joinFilePath(__dirname, '/../../../config/config-default.json')), undefined, { variables: { @@ -48,7 +48,7 @@ describe('CliRunner', (): void => { 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/', 'urn:solid-server:default:variable:sparqlEndpoint': undefined, 'urn:solid-server:default:variable:loggingLevel': 'info', - 'urn:solid-server:default:variable:podTemplateFolder': path.join(__dirname, '../../../templates'), + 'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../../../templates'), }, }, ); @@ -77,18 +77,18 @@ describe('CliRunner', (): void => { expect(Loader).toHaveBeenCalledTimes(1); expect(Loader).toHaveBeenCalledWith({ - mainModulePath: '/var/cwd/module/path', + mainModulePath: toSystemFilePath('/var/cwd/module/path'), scanGlobal: true, }); expect(loader.instantiateFromUrl).toHaveBeenCalledWith( 'urn:solid-server:default:Initializer', - '/var/cwd/myconfig.json', + toSystemFilePath('/var/cwd/myconfig.json'), undefined, { variables: { 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', 'urn:solid-server:default:variable:loggingLevel': 'debug', - 'urn:solid-server:default:variable:podTemplateFolder': 'templates', + 'urn:solid-server:default:variable:podTemplateFolder': '/var/cwd/templates', 'urn:solid-server:default:variable:port': 4000, 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root', 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', @@ -116,18 +116,18 @@ describe('CliRunner', (): void => { expect(Loader).toHaveBeenCalledTimes(1); expect(Loader).toHaveBeenCalledWith({ - mainModulePath: '/var/cwd/module/path', + mainModulePath: toSystemFilePath('/var/cwd/module/path'), scanGlobal: true, }); expect(loader.instantiateFromUrl).toHaveBeenCalledWith( 'urn:solid-server:default:Initializer', - '/var/cwd/myconfig.json', + toSystemFilePath('/var/cwd/myconfig.json'), undefined, { variables: { 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', 'urn:solid-server:default:variable:loggingLevel': 'debug', - 'urn:solid-server:default:variable:podTemplateFolder': 'templates', + 'urn:solid-server:default:variable:podTemplateFolder': '/var/cwd/templates', 'urn:solid-server:default:variable:port': 4000, 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root', 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', diff --git a/test/unit/util/PathUtil.test.ts b/test/unit/util/PathUtil.test.ts index 27d545ef15..abd2f04fbb 100644 --- a/test/unit/util/PathUtil.test.ts +++ b/test/unit/util/PathUtil.test.ts @@ -1,11 +1,41 @@ +import { sep } from 'path'; import { decodeUriPathComponents, encodeUriPathComponents, ensureTrailingSlash, + joinFilePath, + normalizeFilePath, toCanonicalUriPath, + toSystemFilePath, } from '../../../src/util/PathUtil'; describe('PathUtil', (): void => { + describe('normalizeFilePath', (): void => { + it('normalizes POSIX paths.', async(): Promise => { + expect(normalizeFilePath('/foo/bar/../baz')).toEqual('/foo/baz'); + }); + + it('normalizes Windows paths.', async(): Promise => { + expect(normalizeFilePath('c:\\foo\\bar\\..\\baz')).toEqual('c:/foo/baz'); + }); + }); + + describe('joinFilePath', (): void => { + it('joins POSIX paths.', async(): Promise => { + expect(joinFilePath('/foo/bar/', '..', '/baz')).toEqual('/foo/baz'); + }); + + it('joins Windows paths.', async(): Promise => { + expect(joinFilePath('c:\\foo\\bar\\', '..', '/baz')).toEqual(`c:/foo/baz`); + }); + }); + + describe('toSystemFilePath', (): void => { + it('converts a POSIX path to an OS-specific path.', async(): Promise => { + expect(toSystemFilePath('c:/foo/bar/')).toEqual(`c:${sep}foo${sep}bar${sep}`); + }); + }); + describe('#ensureTrailingSlash', (): void => { it('makes sure there is always exactly 1 slash.', async(): Promise => { expect(ensureTrailingSlash('http://test.com')).toEqual('http://test.com/'); diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index aa34e4812c..7f1c5847c6 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -1,13 +1,12 @@ import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; import type { IncomingHttpHeaders } from 'http'; -import { join } from 'path'; import { Readable } from 'stream'; import * as url from 'url'; import type { MockResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http'; import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/'; -import { guardedStreamFrom, RepresentationMetadata, ensureTrailingSlash } from '../../src/'; +import { guardedStreamFrom, RepresentationMetadata, joinFilePath, ensureTrailingSlash } from '../../src/'; import { CONTENT_TYPE } from '../../src/util/Vocabularies'; import { performRequest } from './Util'; @@ -109,7 +108,7 @@ export class ResourceHelper { public async createResource(fileLocation: string, slug: string, contentType: string, mayFail = false): Promise> { const fileData = await fs.readFile( - join(__dirname, fileLocation), + joinFilePath(__dirname, fileLocation), ); const response: MockResponse = await this.performRequestWithBody( @@ -131,7 +130,7 @@ export class ResourceHelper { public async replaceResource(fileLocation: string, requestUrl: string, contentType: string): Promise> { const fileData = await fs.readFile( - join(__dirname, fileLocation), + joinFilePath(__dirname, fileLocation), ); const putUrl = new URL(requestUrl);