Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
408 lines
10 KiB
JavaScript
408 lines
10 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
// A browser shim for the Node `path` builtin, exposing only the subset used by
|
|
// `lib/util/path.js`: `posix.normalize`, `posix.dirname`, `win32.normalize`,
|
|
// `win32.dirname` and a (posix) `basename`. It is a faithful port of Node's
|
|
// `path` implementation and is wired in through the package `browser` field, so
|
|
// Node, Deno and Bun keep using their native `path` and only browser bundles
|
|
// use this. Inputs are always strings here, so Node's `validateString` guards
|
|
// are omitted.
|
|
|
|
const CHAR_UPPERCASE_A = 65;
|
|
const CHAR_UPPERCASE_Z = 90;
|
|
const CHAR_LOWERCASE_A = 97;
|
|
const CHAR_LOWERCASE_Z = 122;
|
|
const CHAR_DOT = 46;
|
|
const CHAR_FORWARD_SLASH = 47;
|
|
const CHAR_BACKWARD_SLASH = 92;
|
|
const CHAR_COLON = 58;
|
|
|
|
/**
|
|
* @param {number} code char code
|
|
* @returns {boolean} true for `/`
|
|
*/
|
|
const isPosixPathSeparator = (code) => code === CHAR_FORWARD_SLASH;
|
|
|
|
/**
|
|
* @param {number} code char code
|
|
* @returns {boolean} true for `/` or `\`
|
|
*/
|
|
const isPathSeparator = (code) =>
|
|
code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
|
|
|
|
/**
|
|
* @param {number} code char code
|
|
* @returns {boolean} true for an ASCII letter (a windows device root)
|
|
*/
|
|
const isWindowsDeviceRoot = (code) =>
|
|
(code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
|
|
(code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z);
|
|
|
|
/**
|
|
* Resolve `.` and `..` segments in a path. Ported from Node's internal
|
|
* `normalizeString`.
|
|
* @param {string} path path
|
|
* @param {boolean} allowAboveRoot whether leading `..` may be kept
|
|
* @param {string} separator path separator to emit
|
|
* @param {(code: number) => boolean} isSeparator separator predicate
|
|
* @returns {string} normalized path string (without root)
|
|
*/
|
|
function normalizeString(path, allowAboveRoot, separator, isSeparator) {
|
|
let res = "";
|
|
let lastSegmentLength = 0;
|
|
let lastSlash = -1;
|
|
let dots = 0;
|
|
let code = 0;
|
|
for (let i = 0; i <= path.length; ++i) {
|
|
if (i < path.length) {
|
|
code = path.charCodeAt(i);
|
|
} else if (isSeparator(code)) {
|
|
break;
|
|
} else {
|
|
code = CHAR_FORWARD_SLASH;
|
|
}
|
|
|
|
if (isSeparator(code)) {
|
|
if (lastSlash === i - 1 || dots === 1) {
|
|
// NOOP
|
|
} else if (dots === 2) {
|
|
if (
|
|
res.length < 2 ||
|
|
lastSegmentLength !== 2 ||
|
|
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
|
|
res.charCodeAt(res.length - 2) !== CHAR_DOT
|
|
) {
|
|
if (res.length > 2) {
|
|
const lastSlashIndex = res.lastIndexOf(separator);
|
|
if (lastSlashIndex === -1) {
|
|
res = "";
|
|
lastSegmentLength = 0;
|
|
} else {
|
|
res = res.slice(0, lastSlashIndex);
|
|
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
|
|
}
|
|
lastSlash = i;
|
|
dots = 0;
|
|
continue;
|
|
} else if (res.length !== 0) {
|
|
res = "";
|
|
lastSegmentLength = 0;
|
|
lastSlash = i;
|
|
dots = 0;
|
|
continue;
|
|
}
|
|
}
|
|
if (allowAboveRoot) {
|
|
res += res.length > 0 ? `${separator}..` : "..";
|
|
lastSegmentLength = 2;
|
|
}
|
|
} else {
|
|
if (res.length > 0) {
|
|
res += `${separator}${path.slice(lastSlash + 1, i)}`;
|
|
} else {
|
|
res = path.slice(lastSlash + 1, i);
|
|
}
|
|
lastSegmentLength = i - lastSlash - 1;
|
|
}
|
|
lastSlash = i;
|
|
dots = 0;
|
|
} else if (code === CHAR_DOT && dots !== -1) {
|
|
++dots;
|
|
} else {
|
|
dots = -1;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* @param {string} path path
|
|
* @returns {string} normalized posix path
|
|
*/
|
|
function posixNormalize(path) {
|
|
if (path.length === 0) return ".";
|
|
|
|
const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
|
|
const trailingSeparator =
|
|
path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
|
|
|
|
path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator);
|
|
|
|
if (path.length === 0) {
|
|
if (isAbsolute) return "/";
|
|
return trailingSeparator ? "./" : ".";
|
|
}
|
|
if (trailingSeparator) path += "/";
|
|
|
|
return isAbsolute ? `/${path}` : path;
|
|
}
|
|
|
|
/**
|
|
* @param {string} path path
|
|
* @returns {string} posix dirname
|
|
*/
|
|
function posixDirname(path) {
|
|
if (path.length === 0) return ".";
|
|
const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
|
|
let end = -1;
|
|
let matchedSlash = true;
|
|
for (let i = path.length - 1; i >= 1; --i) {
|
|
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
|
|
if (!matchedSlash) {
|
|
end = i;
|
|
break;
|
|
}
|
|
} else {
|
|
matchedSlash = false;
|
|
}
|
|
}
|
|
|
|
if (end === -1) return hasRoot ? "/" : ".";
|
|
if (hasRoot && end === 1) return "//";
|
|
return path.slice(0, end);
|
|
}
|
|
|
|
/**
|
|
* Normalizes drive paths (`C:\…`), UNC paths (`\\server\share\…`), DOS device
|
|
* paths (`\\.\…`, `\\?\…`) and relative/normal segments like Node's
|
|
* `path.win32.normalize`, including the CVE-2024-36139 colon-segment guard.
|
|
*
|
|
* Scope note: reserved Windows device names (`CON`, `COM1`, `LPT1`, …) are not
|
|
* special-cased, so e.g. `\\.\COM1:` differs from Node. Such names cannot occur
|
|
* in a browser (the only place this shim is used) and the resolver never routes
|
|
* them to `win32.normalize`, so this does not affect resolution.
|
|
* @param {string} path path
|
|
* @returns {string} normalized win32 path
|
|
*/
|
|
function win32Normalize(path) {
|
|
const len = path.length;
|
|
if (len === 0) return ".";
|
|
let rootEnd = 0;
|
|
/** @type {string | undefined} */
|
|
let device;
|
|
let isAbsolute = false;
|
|
const code = path.charCodeAt(0);
|
|
|
|
if (len === 1) {
|
|
return isPosixPathSeparator(code) ? "\\" : path;
|
|
}
|
|
|
|
if (isPathSeparator(code)) {
|
|
// Possible UNC root; an initial separator means an absolute path.
|
|
isAbsolute = true;
|
|
|
|
if (isPathSeparator(path.charCodeAt(1))) {
|
|
// Matched double path separator at beginning
|
|
let j = 2;
|
|
let last = j;
|
|
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j < len && j !== last) {
|
|
const firstPart = path.slice(last, j);
|
|
last = j;
|
|
while (j < len && isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j < len && j !== last) {
|
|
last = j;
|
|
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j === len || j !== last) {
|
|
if (firstPart === "." || firstPart === "?") {
|
|
// Device root, e.g. `\\.\pipe\…` or `\\?\C:\…`
|
|
device = `\\\\${firstPart}`;
|
|
rootEnd = 4;
|
|
} else if (j === len) {
|
|
// Matched a UNC root only
|
|
return `\\\\${firstPart}\\${path.slice(last)}\\`;
|
|
} else {
|
|
// Matched a UNC root with leftovers
|
|
device = `\\\\${firstPart}\\${path.slice(last, j)}`;
|
|
rootEnd = j;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
rootEnd = 1;
|
|
}
|
|
} else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
|
|
// Possible device root
|
|
device = path.slice(0, 2);
|
|
rootEnd = 2;
|
|
if (len > 2 && isPathSeparator(path.charCodeAt(2))) {
|
|
isAbsolute = true;
|
|
rootEnd = 3;
|
|
}
|
|
}
|
|
|
|
let tail =
|
|
rootEnd < len
|
|
? normalizeString(path.slice(rootEnd), !isAbsolute, "\\", isPathSeparator)
|
|
: "";
|
|
if (tail.length === 0 && !isAbsolute) tail = ".";
|
|
if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) {
|
|
tail += "\\";
|
|
}
|
|
if (!isAbsolute && device === undefined && path.includes(":")) {
|
|
// A relative path that wasn't resolved to a device must not turn into
|
|
// something Windows could read as an absolute/drive path (CVE-2024-36139).
|
|
if (
|
|
tail.length >= 2 &&
|
|
isWindowsDeviceRoot(tail.charCodeAt(0)) &&
|
|
tail.charCodeAt(1) === CHAR_COLON
|
|
) {
|
|
return `.\\${tail}`;
|
|
}
|
|
let index = path.indexOf(":");
|
|
do {
|
|
if (index === len - 1 || isPathSeparator(path.charCodeAt(index + 1))) {
|
|
return `.\\${tail}`;
|
|
}
|
|
} while ((index = path.indexOf(":", index + 1)) !== -1);
|
|
}
|
|
if (device === undefined) {
|
|
return isAbsolute ? `\\${tail}` : tail;
|
|
}
|
|
return isAbsolute ? `${device}\\${tail}` : `${device}${tail}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} path path
|
|
* @returns {string} win32 dirname
|
|
*/
|
|
function win32Dirname(path) {
|
|
const len = path.length;
|
|
if (len === 0) return ".";
|
|
let rootEnd = -1;
|
|
let offset = 0;
|
|
const code = path.charCodeAt(0);
|
|
|
|
if (len === 1) {
|
|
return isPathSeparator(code) ? path : ".";
|
|
}
|
|
|
|
if (isPathSeparator(code)) {
|
|
// Possible UNC root
|
|
rootEnd = offset = 1;
|
|
|
|
if (isPathSeparator(path.charCodeAt(1))) {
|
|
let j = 2;
|
|
let last = j;
|
|
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j < len && j !== last) {
|
|
last = j;
|
|
while (j < len && isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j < len && j !== last) {
|
|
last = j;
|
|
while (j < len && !isPathSeparator(path.charCodeAt(j))) j++;
|
|
if (j === len) {
|
|
// Matched a UNC root only
|
|
return path;
|
|
}
|
|
if (j !== last) {
|
|
// Matched a UNC root with leftovers
|
|
rootEnd = offset = j + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (isWindowsDeviceRoot(code) && path.charCodeAt(1) === CHAR_COLON) {
|
|
rootEnd = len > 2 && isPathSeparator(path.charCodeAt(2)) ? 3 : 2;
|
|
offset = rootEnd;
|
|
}
|
|
|
|
let end = -1;
|
|
let matchedSlash = true;
|
|
for (let i = len - 1; i >= offset; --i) {
|
|
if (isPathSeparator(path.charCodeAt(i))) {
|
|
if (!matchedSlash) {
|
|
end = i;
|
|
break;
|
|
}
|
|
} else {
|
|
matchedSlash = false;
|
|
}
|
|
}
|
|
|
|
if (end === -1) {
|
|
if (rootEnd === -1) return ".";
|
|
end = rootEnd;
|
|
}
|
|
return path.slice(0, end);
|
|
}
|
|
|
|
/**
|
|
* Posix `basename` — the browser is treated as a posix platform, matching how
|
|
* Node picks the posix variant for `path.basename` on non-Windows systems.
|
|
* @param {string} path path
|
|
* @param {string=} suffix optional suffix to strip
|
|
* @returns {string} basename
|
|
*/
|
|
function basename(path, suffix) {
|
|
let start = 0;
|
|
let end = -1;
|
|
let matchedSlash = true;
|
|
|
|
if (
|
|
suffix !== undefined &&
|
|
suffix.length > 0 &&
|
|
suffix.length <= path.length
|
|
) {
|
|
if (suffix === path) return "";
|
|
let extIdx = suffix.length - 1;
|
|
let firstNonSlashEnd = -1;
|
|
for (let i = path.length - 1; i >= 0; --i) {
|
|
const code = path.charCodeAt(i);
|
|
if (code === CHAR_FORWARD_SLASH) {
|
|
if (!matchedSlash) {
|
|
start = i + 1;
|
|
break;
|
|
}
|
|
} else {
|
|
if (firstNonSlashEnd === -1) {
|
|
matchedSlash = false;
|
|
firstNonSlashEnd = i + 1;
|
|
}
|
|
if (extIdx >= 0) {
|
|
if (code === suffix.charCodeAt(extIdx)) {
|
|
if (--extIdx === -1) {
|
|
end = i;
|
|
}
|
|
} else {
|
|
extIdx = -1;
|
|
end = firstNonSlashEnd;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (start === end) {
|
|
end = firstNonSlashEnd;
|
|
} else if (end === -1) {
|
|
end = path.length;
|
|
}
|
|
return path.slice(start, end);
|
|
}
|
|
for (let i = path.length - 1; i >= 0; --i) {
|
|
if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
|
|
if (!matchedSlash) {
|
|
start = i + 1;
|
|
break;
|
|
}
|
|
} else if (end === -1) {
|
|
matchedSlash = false;
|
|
end = i + 1;
|
|
}
|
|
}
|
|
|
|
if (end === -1) return "";
|
|
return path.slice(start, end);
|
|
}
|
|
|
|
module.exports = {
|
|
basename,
|
|
posix: { normalize: posixNormalize, dirname: posixDirname },
|
|
win32: { normalize: win32Normalize, dirname: win32Dirname },
|
|
};
|