File size: 3,640 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * Session tool path normalization helpers.
 *
 * Expands user/file URL inputs and resolves read/write paths against the active cwd with macOS filename variants.
 */
import { basename, isAbsolute, resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
import { expandHomePrefix, resolveOsHomeDir } from "../../../infra/home-dir.js";
import { preserveAtPrefixedRelativePath } from "../../path-policy.js";
import { normalizeFileReferencePrefix } from "../../sandbox-paths.js";

const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
const NARROW_NO_BREAK_SPACE = "\u202F";
function normalizeUnicodeSpaces(str: string): string {
  return str.replace(UNICODE_SPACES, " ");
}

function tryMacOSScreenshotPath(filePath: string): string {
  return filePath.replace(/ (?=(?:AM|PM)(?:\b|\.))/gi, NARROW_NO_BREAK_SPACE);
}

/** Expand OS-home syntax without treating a POSIX backslash as a separator. */
export function expandOsHomePrefix(filePath: string): string {
  const isHomePath =
    filePath === "~" ||
    filePath.startsWith("~/") ||
    (process.platform === "win32" && filePath.startsWith("~\\"));
  if (!isHomePath) {
    return filePath;
  }
  const home = resolveOsHomeDir();
  return home ? expandHomePrefix(filePath, { home }) : filePath;
}

function expandPath(filePath: string): string {
  const normalized = normalizeFileReferencePrefix(filePath);
  if (normalized.startsWith("file://")) {
    try {
      return fileURLToPath(normalized);
    } catch {
      return normalized;
    }
  }
  return expandOsHomePrefix(normalized);
}

/**
 * Resolve a path relative to the given cwd.
 * Handles ~ expansion and absolute paths.
 */
export function resolveToCwd(filePath: string, cwd: string): string {
  const expanded = expandPath(filePath);
  return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
}

/** Resolve local file paths using the filesystem that owns literal @ names. */
export function resolveLocalPathToCwd(filePath: string, cwd: string): string {
  return resolveToCwd(preserveAtPrefixedRelativePath(filePath, cwd), cwd);
}

function collectReadPathVariants(filePath: string, includeNfd: boolean): string[] {
  const variants = new Set<string>();
  const fileName = basename(filePath);
  const parentPrefix = filePath.slice(0, filePath.length - fileName.length);
  // The caller may already have authorized the parent directory. Only vary the
  // basename so a fallback cannot escape that validated boundary.
  const asciiSpace = normalizeUnicodeSpaces(fileName);
  for (const spaced of [asciiSpace, tryMacOSScreenshotPath(asciiSpace)]) {
    const straightQuotes = spaced.replace(/[\u2018\u2019]/g, "'");
    const curlyQuotes = spaced.replace(/['\u2018]/g, "\u2019");
    for (const quoted of [straightQuotes, curlyQuotes]) {
      variants.add(`${parentPrefix}${quoted.normalize("NFC")}`);
      // macOS filesystems resolve NFC/NFD spellings to the same entry; probing both
      // makes one file look ambiguous. Other platforms can store both distinctly.
      if (includeNfd) {
        variants.add(`${parentPrefix}${quoted.normalize("NFD")}`);
      }
    }
  }
  variants.delete(filePath);
  return [...variants];
}

/** Equivalent filename spellings worth probing after an exact read path misses. */
export function getReadPathVariants(filePath: string): string[] {
  return collectReadPathVariants(filePath, process.platform !== "darwin");
}

/** Every spelling an exact read or its fallback probes can accept. */
export function getReadQueuePaths(filePath: string): string[] {
  return [filePath, ...collectReadPathVariants(filePath, true)];
}