98 lines · 3.4 KB · TypeScript
RawDownload
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) };}