From 621a93851ca61ee58a9dc92a667fccf6ebfd71a1 Mon Sep 17 00:00:00 2001 From: William Chargin Date: Wed, 15 Aug 2018 13:03:19 -0700 Subject: [PATCH] Resolve a relative path to the application root (#665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This is necessary for #643. If we’re serving `/prototype/index.html`, we need to to use `..` to refer to the root of the site. This patch adds `rootFromPath`, which performs the relevant transformation. (The implementation is trivial, but figuring out exactly what the specification should be was not!) Test Plan: Unit tests added; `yarn test` suffices. wchargin-branch: rootFromPath --- src/app/assets.js | 23 ++++++++++++++++ src/app/assets.test.js | 60 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/assets.js b/src/app/assets.js index 6928ab6..b00c70c 100644 --- a/src/app/assets.js +++ b/src/app/assets.js @@ -40,3 +40,26 @@ export class Assets { return normalize(`${this._getRoot()}/${path}`); } } + +/** + * Given an absolute path `p`, return a relative path `r` such that a + * web page at pathname `p` should use `r` to refer to the root of the + * application. The result will only contain components `.` and `..`. + * + * Examples: + * + * - "/foo/" maps to ".."; + * - "/foo/bar" also maps to ".."; + * - "/foo/bar/" maps to "../.."; + * - "/" maps to ".". + * + * If the argument does not start with "/", an error will be thrown. + */ +export function rootFromPath(path: string) { + const normalized = normalize(path); + if (normalized[0] !== "/") { + throw new Error("expected absolute path: " + JSON.stringify(path)); + } + const levels = (normalized.match(/\//g) || []).length; + return normalize(new Array(levels - 1).fill("..").join("/")); +} diff --git a/src/app/assets.test.js b/src/app/assets.test.js index fbde3e9..fb57ea6 100644 --- a/src/app/assets.test.js +++ b/src/app/assets.test.js @@ -1,6 +1,6 @@ // @flow -import {Assets} from "./assets"; +import {Assets, rootFromPath} from "./assets"; describe("app/assets", () => { describe("Assets", () => { @@ -161,4 +161,62 @@ describe("app/assets", () => { }); }); }); + + describe("rootFromPath", () => { + it("throws on the empty path", () => { + expect(() => rootFromPath("")).toThrow('expected absolute path: ""'); + }); + it('throws on an implicitly relative path ("wat")', () => { + expect(() => rootFromPath("wat")).toThrow( + 'expected absolute path: "wat"' + ); + }); + it('throws on an explicitly relative path ("./wat")', () => { + expect(() => rootFromPath("./wat")).toThrow( + 'expected absolute path: "./wat"' + ); + }); + describe('returns "." for a path at root', () => { + it('with no file component ("/")', () => { + expect(rootFromPath("/")).toEqual("."); + }); + it('with a file component ("/index.html")', () => { + expect(rootFromPath("/index.html")).toEqual("."); + }); + it('with superfluous slashes ("///")', () => { + expect(rootFromPath("///")).toEqual("."); + }); + it('with indirection, like "/foo/../"', () => { + expect(rootFromPath("/foo/../")).toEqual("."); + }); + }); + describe('returns ".." for a path one level deep', () => { + it('with no file component ("/foo/")', () => { + expect(rootFromPath("/foo/")).toEqual(".."); + }); + it('with a file component ("/foo/index.html")', () => { + expect(rootFromPath("/foo/index.html")).toEqual(".."); + }); + it('with superfluous slashes ("//foo//")', () => { + expect(rootFromPath("//foo//")).toEqual(".."); + }); + it('with indirection, like "/foo/bar/../"', () => { + expect(rootFromPath("/foo/bar/../")).toEqual(".."); + }); + }); + describe('returns "../.." for a path two levels deep', () => { + it('with no file component ("/foo/bar/")', () => { + expect(rootFromPath("/foo/bar/")).toEqual("../.."); + }); + it('with a file component ("/foo/bar/index.html")', () => { + expect(rootFromPath("/foo/bar/index.html")).toEqual("../.."); + }); + it('with superfluous slashes ("//foo//bar//")', () => { + expect(rootFromPath("//foo//bar//")).toEqual("../.."); + }); + it('with indirection, like "/foo/bar/baz/../"', () => { + expect(rootFromPath("/foo/bar/baz/../")).toEqual("../.."); + }); + }); + }); });