const DIRECT_IMAGE_SOURCE_PATTERN = /^(?:https?:|data:|blob:|\/\/)/i;const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/; export type MarkdownImageSource = | { readonly _tag: "Direct"; readonly uri: string } | { readonly _tag: "WorkspaceFile"; readonly path: string } | { readonly _tag: "Blocked" }; function safeDecode(value: string): string { try { return decodeURIComponent(value); } catch { return value; }} function normalizeSource(value: string): string { const trimmed = value.trim(); return trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed;} function normalizeWindowsDrivePath(value: string): string { return /^\/[A-Za-z]:[\\/]/.test(value) ? value.slice(1) : value;} function parseFileUrl(value: string): string | null { try { const parsed = new URL(value); if (parsed.protocol.toLowerCase() !== "file:") return null; if (parsed.hostname.length > 0 && parsed.hostname.toLowerCase() !== "localhost") { const pathname = safeDecode(parsed.pathname).replaceAll("/", "\\"); return `\\\\${safeDecode(parsed.hostname)}${pathname}`; } const pathname = safeDecode(parsed.pathname); if (pathname.length === 0) return null; return normalizeWindowsDrivePath(pathname); } catch { return null; }} function stripSearchAndHash(value: string): string { const searchIndex = value.indexOf("?"); const hashIndex = value.indexOf("#"); const end = [searchIndex, hashIndex] .filter((index) => index >= 0) .reduce((lowest, index) => Math.min(lowest, index), value.length); return value.slice(0, end);} function joinWorkspacePath(workspaceRoot: string, relativePath: string): string { const windows = WINDOWS_DRIVE_PATH_PATTERN.test(workspaceRoot) || workspaceRoot.startsWith("\\\\"); const separator = windows ? "\\" : "/"; const root = workspaceRoot.replace(/[\\/]+$/, ""); const path = relativePath.replace(/[\\/]/g, separator).replace(/^[\\/]+/, ""); return `${root}${separator}${path}`;} /** * Classifies a markdown image source by where its bytes must be loaded from. * Filesystem paths belong to the environment host and must never reach a * browser or native image component without first becoming a signed asset URL. */export function classifyMarkdownImageSource( value: string | null | undefined, workspaceRoot?: string | null,): MarkdownImageSource { if (value === null || value === undefined) return { _tag: "Blocked" }; const source = normalizeSource(value); if (source.length === 0 || source.startsWith("#") || source.startsWith("?")) { return { _tag: "Blocked" }; } if (DIRECT_IMAGE_SOURCE_PATTERN.test(source)) { return { _tag: "Direct", uri: source }; } if (/^file:/i.test(source)) { const path = parseFileUrl(source); return path === null ? { _tag: "Blocked" } : { _tag: "WorkspaceFile", path }; } const path = normalizeWindowsDrivePath(safeDecode(stripSearchAndHash(source))); if (path.length === 0) return { _tag: "Blocked" }; if (path.startsWith("/") || WINDOWS_DRIVE_PATH_PATTERN.test(path) || path.startsWith("\\\\")) { return { _tag: "WorkspaceFile", path }; } if (URI_SCHEME_PATTERN.test(path) || path.startsWith("~/") || path.startsWith("~\\")) { return { _tag: "Blocked" }; } if (!workspaceRoot) return { _tag: "Blocked" }; return { _tag: "WorkspaceFile", path: joinWorkspacePath(workspaceRoot, path) };}
const DIRECT_IMAGE_SOURCE_PATTERN = /^(?:https?:|data:|blob:|\/\/)/i;
const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
export type MarkdownImageSource =
| { readonly _tag: "Direct"; readonly uri: string }
| { readonly _tag: "WorkspaceFile"; readonly path: string }
| { readonly _tag: "Blocked" };
function safeDecode(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function normalizeSource(value: string): string {
const trimmed = value.trim();
return trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed;
}
function normalizeWindowsDrivePath(value: string): string {
return /^\/[A-Za-z]:[\\/]/.test(value) ? value.slice(1) : value;
}
function parseFileUrl(value: string): string | null {
try {
const parsed = new URL(value);
if (parsed.protocol.toLowerCase() !== "file:") return null;
if (parsed.hostname.length > 0 && parsed.hostname.toLowerCase() !== "localhost") {
const pathname = safeDecode(parsed.pathname).replaceAll("/", "\\");
return `\\\\${safeDecode(parsed.hostname)}${pathname}`;
}
const pathname = safeDecode(parsed.pathname);
if (pathname.length === 0) return null;
return normalizeWindowsDrivePath(pathname);
} catch {
return null;
}
}
function stripSearchAndHash(value: string): string {
const searchIndex = value.indexOf("?");
const hashIndex = value.indexOf("#");
const end = [searchIndex, hashIndex]
.filter((index) => index >= 0)
.reduce((lowest, index) => Math.min(lowest, index), value.length);
return value.slice(0, end);
}
function joinWorkspacePath(workspaceRoot: string, relativePath: string): string {
const windows =
WINDOWS_DRIVE_PATH_PATTERN.test(workspaceRoot) || workspaceRoot.startsWith("\\\\");
const separator = windows ? "\\" : "/";
const root = workspaceRoot.replace(/[\\/]+$/, "");
const path = relativePath.replace(/[\\/]/g, separator).replace(/^[\\/]+/, "");
return `${root}${separator}${path}`;
}
/**
* Classifies a markdown image source by where its bytes must be loaded from.
* Filesystem paths belong to the environment host and must never reach a
* browser or native image component without first becoming a signed asset URL.
*/
export function classifyMarkdownImageSource(
value: string | null | undefined,
workspaceRoot?: string | null,
): MarkdownImageSource {
if (value === null || value === undefined) return { _tag: "Blocked" };
const source = normalizeSource(value);
if (source.length === 0 || source.startsWith("#") || source.startsWith("?")) {
return { _tag: "Blocked" };
}
if (DIRECT_IMAGE_SOURCE_PATTERN.test(source)) {
return { _tag: "Direct", uri: source };
}
if (/^file:/i.test(source)) {
const path = parseFileUrl(source);
return path === null ? { _tag: "Blocked" } : { _tag: "WorkspaceFile", path };
}
const path = normalizeWindowsDrivePath(safeDecode(stripSearchAndHash(source)));
if (path.length === 0) return { _tag: "Blocked" };
if (path.startsWith("/") || WINDOWS_DRIVE_PATH_PATTERN.test(path) || path.startsWith("\\\\")) {
return { _tag: "WorkspaceFile", path };
}
if (URI_SCHEME_PATTERN.test(path) || path.startsWith("~/") || path.startsWith("~\\")) {
return { _tag: "Blocked" };
}
if (!workspaceRoot) return { _tag: "Blocked" };
return { _tag: "WorkspaceFile", path: joinWorkspacePath(workspaceRoot, path) };
}