MCPcopy
hub / github.com/garrytan/gstack / normalizeFileUrl

Function normalizeFileUrl

browse/src/url-validation.ts:127–214  ·  view source on GitHub ↗
(url: string)

Source from the content-addressed store, hash-verified

125 * trigger Chromium's directory listing, which is a different product surface.
126 */
127export function normalizeFileUrl(url: string): string {
128 if (!url.toLowerCase().startsWith('file:')) return url;
129
130 // Split off query + fragment BEFORE touching the path — SPAs + fixture URLs rely
131 // on these. path.resolve would URL-encode `?` and `#` as `%3F`/`%23` (and
132 // pathToFileURL drops them entirely), silently routing preview URLs to the
133 // wrong fixture. Extract, normalize the path, reattach at the end.
134 //
135 // Parse order: `?` before `#` per RFC 3986 — '?' in a fragment is literal.
136 // Find the FIRST `?` or `#`, whichever comes first, and take everything
137 // after (including the delimiter) as the trailing segment.
138 const qIdx = url.indexOf('?');
139 const hIdx = url.indexOf('#');
140 let delimIdx = -1;
141 if (qIdx >= 0 && hIdx >= 0) delimIdx = Math.min(qIdx, hIdx);
142 else if (qIdx >= 0) delimIdx = qIdx;
143 else if (hIdx >= 0) delimIdx = hIdx;
144
145 const pathPart = delimIdx >= 0 ? url.slice(0, delimIdx) : url;
146 const trailing = delimIdx >= 0 ? url.slice(delimIdx) : '';
147
148 const rest = pathPart.slice('file:'.length);
149
150 // file:/// or longer → standard absolute; pass through unchanged (caller validates path).
151 if (rest.startsWith('///')) {
152 // Reject bare root-only (file:/// with nothing after)
153 if (rest === '///' || rest === '////') {
154 throw new Error('Invalid file URL: file:/// has no path. Use file:///<absolute-path>.');
155 }
156 return pathPart + trailing;
157 }
158
159 // Everything else: must start with // (we accept file://... only)
160 if (!rest.startsWith('//')) {
161 throw new Error(`Invalid file URL: ${url}. Use file:///<absolute-path> or file://./<rel> or file://~/<rel>.`);
162 }
163
164 const afterDoubleSlash = rest.slice(2);
165
166 // Reject empty (file://) and trailing-slash-only (file://./ listing cwd).
167 if (afterDoubleSlash === '') {
168 throw new Error('Invalid file URL: file:// is empty. Use file:///<absolute-path>.');
169 }
170 if (afterDoubleSlash === '.' || afterDoubleSlash === './') {
171 throw new Error('Invalid file URL: file://./ would list the current directory. Use file://./<filename> to render a specific file.');
172 }
173 if (afterDoubleSlash === '~' || afterDoubleSlash === '~/') {
174 throw new Error('Invalid file URL: file://~/ would list the home directory. Use file://~/<filename> to render a specific file.');
175 }
176
177 // Home-relative: file://~/<rel>
178 if (afterDoubleSlash.startsWith('~/')) {
179 const rel = afterDoubleSlash.slice(2);
180 const absPath = path.join(os.homedir(), rel);
181 return pathToFileURL(absPath).href + trailing;
182 }
183
184 // cwd-relative with explicit ./ : file://./<rel>

Callers 2

validateNavigationUrlFunction · 0.85

Calls

no outgoing calls

Tested by

no test coverage detected