notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

path_normalize.rs (5540B)


      1 //! Path normalization for JSONL source-data.
      2 //!
      3 //! When storing JSONL lines in nostr events, absolute paths are converted to
      4 //! relative (using the session's `cwd` as base). On reconstruction, relative
      5 //! paths are re-expanded using the local machine's working directory.
      6 //!
      7 //! This operates on the raw JSON string via string replacement — paths can
      8 //! appear anywhere in tool inputs/outputs, so structural replacement would
      9 //! miss nested occurrences.
     10 
     11 /// Replace all occurrences of `cwd` prefix in absolute paths with relative paths.
     12 ///
     13 /// Not currently used (Phase 1 stores raw paths), kept for future Phase 2.
     14 #[allow(dead_code)]
     15 ///
     16 /// For example, with cwd = "/Users/jb55/dev/notedeck":
     17 ///   "/Users/jb55/dev/notedeck/src/main.rs" → "src/main.rs"
     18 ///   "/Users/jb55/dev/notedeck" → "."
     19 pub fn normalize_paths(json: &str, cwd: &str) -> String {
     20     if cwd.is_empty() {
     21         return json.to_string();
     22     }
     23 
     24     // Ensure cwd doesn't have a trailing slash for consistent matching
     25     let cwd = cwd.strip_suffix('/').unwrap_or(cwd);
     26 
     27     // Replace "cwd/" prefix first (subpaths), then bare "cwd" (exact match)
     28     let with_slash = format!("{}/", cwd);
     29     let result = json.replace(&with_slash, "");
     30 
     31     // Replace bare cwd (e.g. the cwd field itself) with "."
     32     result.replace(cwd, ".")
     33 }
     34 
     35 /// Re-expand relative paths back to absolute using the given local cwd.
     36 ///
     37 /// Reverses `normalize_paths`: the cwd field "." becomes the local cwd,
     38 /// and relative paths get the cwd prefix prepended.
     39 ///
     40 /// Note: This is not perfectly inverse — it will also expand any unrelated
     41 /// "." occurrences that happen to match. In practice, the cwd field is the
     42 /// main target, and relative paths in tool inputs/outputs are the rest.
     43 ///
     44 /// Not currently used (Phase 1 stores raw paths), kept for future Phase 2.
     45 #[allow(dead_code)]
     46 pub fn denormalize_paths(json: &str, local_cwd: &str) -> String {
     47     if local_cwd.is_empty() {
     48         return json.to_string();
     49     }
     50 
     51     let local_cwd = local_cwd.strip_suffix('/').unwrap_or(local_cwd);
     52 
     53     // We need to be careful about ordering here. We want to:
     54     // 1. Replace "." (bare cwd reference) with the local cwd
     55     // 2. Re-expand relative paths that were stripped of the cwd prefix
     56     //
     57     // But since normalized JSON has paths like "src/main.rs" (no prefix),
     58     // we can't blindly prefix all bare paths. Instead, we reverse the
     59     // exact transformations that normalize_paths applied:
     60     //
     61     // The normalize step replaced:
     62     //   "{cwd}/" → ""  (paths become relative)
     63     //   "{cwd}"  → "." (bare cwd references)
     64     //
     65     // So to reverse, we need context-aware replacement. The safest approach
     66     // is to look for patterns that were likely produced by normalization:
     67     //   - JSON string values that are exactly "." → local_cwd
     68     //   - Relative paths in known field positions
     69     //
     70     // For now, we do simple string replacement which handles the most
     71     // common case (the "cwd" field). Full path reconstruction for tool
     72     // inputs/outputs would need the original field structure.
     73 
     74     // Replace "\"cwd\":\".\"" with the local cwd
     75     let result = json.replace("\"cwd\":\".\"", &format!("\"cwd\":\"{}\"", local_cwd));
     76 
     77     result
     78 }
     79 
     80 #[cfg(test)]
     81 mod tests {
     82     use super::*;
     83 
     84     #[test]
     85     fn test_normalize_absolute_paths() {
     86         let json =
     87             r#"{"cwd":"/Users/jb55/dev/notedeck","file":"/Users/jb55/dev/notedeck/src/main.rs"}"#;
     88         let normalized = normalize_paths(json, "/Users/jb55/dev/notedeck");
     89         assert_eq!(normalized, r#"{"cwd":".","file":"src/main.rs"}"#);
     90     }
     91 
     92     #[test]
     93     fn test_normalize_with_trailing_slash() {
     94         // cwd with trailing slash is stripped; the cwd value in JSON
     95         // still contains the trailing slash so it becomes "" + "/" = "/"
     96         // after replacing the base. In practice JSONL cwd values don't
     97         // have trailing slashes.
     98         let json = r#"{"cwd":"/tmp/project","file":"/tmp/project/lib.rs"}"#;
     99         let normalized = normalize_paths(json, "/tmp/project/");
    100         assert_eq!(normalized, r#"{"cwd":".","file":"lib.rs"}"#);
    101     }
    102 
    103     #[test]
    104     fn test_normalize_empty_cwd() {
    105         let json = r#"{"file":"/some/path"}"#;
    106         let normalized = normalize_paths(json, "");
    107         assert_eq!(normalized, json);
    108     }
    109 
    110     #[test]
    111     fn test_normalize_no_matching_paths() {
    112         let json = r#"{"file":"/other/path/file.rs"}"#;
    113         let normalized = normalize_paths(json, "/Users/jb55/dev/notedeck");
    114         assert_eq!(normalized, json);
    115     }
    116 
    117     #[test]
    118     fn test_normalize_multiple_occurrences() {
    119         let json =
    120             r#"{"old":"/Users/jb55/dev/notedeck/a.rs","new":"/Users/jb55/dev/notedeck/b.rs"}"#;
    121         let normalized = normalize_paths(json, "/Users/jb55/dev/notedeck");
    122         assert_eq!(normalized, r#"{"old":"a.rs","new":"b.rs"}"#);
    123     }
    124 
    125     #[test]
    126     fn test_denormalize_cwd_field() {
    127         let json = r#"{"cwd":"."}"#;
    128         let denormalized = denormalize_paths(json, "/Users/jb55/dev/notedeck");
    129         assert_eq!(denormalized, r#"{"cwd":"/Users/jb55/dev/notedeck"}"#);
    130     }
    131 
    132     #[test]
    133     fn test_normalize_roundtrip_cwd() {
    134         let original_cwd = "/Users/jb55/dev/notedeck";
    135         let json = r#"{"cwd":"/Users/jb55/dev/notedeck"}"#;
    136         let normalized = normalize_paths(json, original_cwd);
    137         assert_eq!(normalized, r#"{"cwd":"."}"#);
    138         let denormalized = denormalize_paths(&normalized, original_cwd);
    139         assert_eq!(denormalized, json);
    140     }
    141 }