refactor: dedupe path lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 22:34:51 +01:00
parent 5a020cf9a1
commit b6970865b6
12 changed files with 41 additions and 20 deletions
+4 -3
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";
import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { sendBlueBubblesAttachment } from "./attachments.js";
import { basenameFromMediaSource, safeFileURLToPath } from "./local-file-access.js";
@@ -73,9 +74,9 @@ function isPathInsideRoot(candidate: string, root: string): boolean {
? normalizedRoot
: normalizedRoot + path.sep;
if (process.platform === "win32") {
const candidateLower = normalizedCandidate.toLowerCase();
const rootLower = normalizedRoot.toLowerCase();
const rootWithSepLower = rootWithSep.toLowerCase();
const candidateLower = lowercasePreservingWhitespace(normalizedCandidate);
const rootLower = lowercasePreservingWhitespace(normalizedRoot);
const rootWithSepLower = lowercasePreservingWhitespace(rootWithSep);
return candidateLower === rootLower || candidateLower.startsWith(rootWithSepLower);
}
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(rootWithSep);
+4 -1
View File
@@ -1,6 +1,7 @@
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
import type { Dirent } from "node:fs";
import { delimiter, dirname, join } from "node:path";
import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime";
import { CLIENT_ID_KEYS, CLIENT_SECRET_KEYS } from "./oauth.shared.js";
type CredentialFs = {
@@ -96,7 +97,9 @@ function resolveGeminiCliDirs(geminiPath: string, resolvedPath: string): string[
for (const candidate of candidates) {
for (const searchDir of resolveGeminiCliSearchDirs(candidate)) {
const key =
process.platform === "win32" ? searchDir.replace(/\\/g, "/").toLowerCase() : searchDir;
process.platform === "win32"
? lowercasePreservingWhitespace(searchDir.replace(/\\/g, "/"))
: searchDir;
if (seen.has(key)) {
continue;
}
+6 -2
View File
@@ -5,6 +5,10 @@ import path from "node:path";
import { Readable, Writable } from "node:stream";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/text-runtime";
export type LobsterEnvelope =
| {
@@ -168,7 +172,7 @@ type PipelineRuntimeContext = {
function normalizeForCwdSandbox(p: string): string {
const normalized = path.normalize(p);
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
return process.platform === "win32" ? lowercasePreservingWhitespace(normalized) : normalized;
}
export function resolveLobsterCwd(cwdRaw: unknown): string {
@@ -305,7 +309,7 @@ async function resolveWorkflowFile(candidate: string, cwd: string) {
if (!fileStat.isFile()) {
throw new Error("Workflow path is not a file");
}
const ext = path.extname(resolved).toLowerCase();
const ext = normalizeLowercaseStringOrEmpty(path.extname(resolved));
if (![".lobster", ".yaml", ".yml", ".json"].includes(ext)) {
throw new Error("Workflow file must end in .lobster, .yaml, .yml, or .json");
}
@@ -18,7 +18,10 @@ import {
type MemoryLightDreamingConfig,
type MemoryRemDreamingConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/text-runtime";
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
import { generateAndAppendDreamNarrative, type NarrativePhaseData } from "./dreaming-narrative.js";
import {
@@ -425,7 +428,7 @@ type SessionIngestionCollectionResult = {
function normalizeWorkspaceKey(workspaceDir: string): string {
const resolved = path.resolve(workspaceDir).replace(/\\/g, "/");
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
return process.platform === "win32" ? lowercasePreservingWhitespace(resolved) : resolved;
}
function resolveSessionIngestionStatePath(workspaceDir: string): string {
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime";
export async function pathExists(filePath: string): Promise<boolean> {
try {
@@ -12,5 +13,7 @@ export async function pathExists(filePath: string): Promise<boolean> {
export async function resolveArtifactKey(absolutePath: string): Promise<string> {
const canonicalPath = await fs.realpath(absolutePath).catch(() => path.resolve(absolutePath));
return process.platform === "win32" ? canonicalPath.toLowerCase() : canonicalPath;
return process.platform === "win32"
? lowercasePreservingWhitespace(canonicalPath)
: canonicalPath;
}
+2 -1
View File
@@ -12,6 +12,7 @@ import {
resolveAgentIdFromSessionKey,
} from "../routing/session-key.js";
import {
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
readStringValue,
resolvePrimaryStringValue,
@@ -293,7 +294,7 @@ function normalizePathForComparison(input: string): string {
// Keep lexical path for non-existent directories.
}
if (process.platform === "win32") {
return normalizeLowercaseStringOrEmpty(normalized);
return lowercasePreservingWhitespace(normalized);
}
return normalized;
}
+3 -3
View File
@@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { detectMime } from "../media/mime.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js";
import { resolveFileWithinRoot } from "./file-resolver.js";
export const A2UI_PATH = "/__openclaw__/a2ui";
@@ -133,7 +133,7 @@ export function injectCanvasLiveReload(html: string): string {
</script>
`.trim();
const idx = normalizeLowercaseStringOrEmpty(html).lastIndexOf("</body>");
const idx = lowercasePreservingWhitespace(html).lastIndexOf("</body>");
if (idx >= 0) {
return `${html.slice(0, idx)}\n${snippet}\n${html.slice(idx)}`;
}
@@ -181,7 +181,7 @@ export async function handleA2uiHttpRequest(
}
try {
const lower = normalizeLowercaseStringOrEmpty(result.realPath);
const lower = lowercasePreservingWhitespace(result.realPath);
const mime =
lower.endsWith(".html") || lower.endsWith(".htm")
? "text/html"
+3 -3
View File
@@ -15,7 +15,7 @@ import { resolveStateDir } from "../config/paths.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { detectMime } from "../media/mime.js";
import type { RuntimeEnv } from "../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js";
import { ensureDir, resolveUserPath } from "../utils.js";
import {
CANVAS_HOST_PATH,
@@ -394,7 +394,7 @@ export async function createCanvasHostHandler(
await handle.close().catch(() => {});
}
const lower = normalizeLowercaseStringOrEmpty(realPath);
const lower = lowercasePreservingWhitespace(realPath);
const mime =
lower.endsWith(".html") || lower.endsWith(".htm")
? "text/html"
@@ -465,7 +465,7 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
const bindHost = opts.listenHost?.trim() || "127.0.0.1";
const server: Server = http.createServer((req, res) => {
if (normalizeLowercaseStringOrEmpty(req.headers.upgrade ?? "") === "websocket") {
if (lowercasePreservingWhitespace(String(req.headers.upgrade ?? "")) === "websocket") {
return;
}
void (async () => {
+2 -1
View File
@@ -3,6 +3,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import type { OpenClawConfig } from "../config/config.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import {
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
@@ -302,7 +303,7 @@ function resolveExecutionConfig(
function normalizePathForComparison(input: string): string {
const normalized = path.resolve(input);
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
return process.platform === "win32" ? lowercasePreservingWhitespace(normalized) : normalized;
}
function formatLocalIsoDay(epochMs: number): string {
+1
View File
@@ -29,6 +29,7 @@ export * from "../utils/reaction-level.js";
export * from "../utils/with-timeout.js";
export {
hasNonEmptyString,
lowercasePreservingWhitespace,
normalizeLowercaseStringOrEmpty,
normalizeNullableString,
normalizeOptionalLowercaseString,
+4
View File
@@ -22,6 +22,10 @@ export function normalizeLowercaseStringOrEmpty(value: unknown): string {
return normalizeOptionalLowercaseString(value) ?? "";
}
export function lowercasePreservingWhitespace(value: string): string {
return value.toLowerCase();
}
export function resolvePrimaryStringValue(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOptionalString(value);
+3 -3
View File
@@ -1,5 +1,5 @@
import type { ServerResponse } from "node:http";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js";
export function createMockServerResponse(): ServerResponse & { body?: string } {
const headers: Record<string, string> = {};
@@ -14,10 +14,10 @@ export function createMockServerResponse(): ServerResponse & { body?: string } {
headersSent: false,
statusCode: 200,
setHeader: (key: string, value: string) => {
headers[normalizeLowercaseStringOrEmpty(key)] = value;
headers[lowercasePreservingWhitespace(key)] = value;
return res;
},
getHeader: (key: string) => headers[normalizeLowercaseStringOrEmpty(key)],
getHeader: (key: string) => headers[lowercasePreservingWhitespace(key)],
end: (body?: string) => {
res.headersSent = true;
res.body = body;