Skip to content

Commit 6170098

Browse files
authored
feat(windows): refactor filesystem handling to support windows (#114)
1 parent e7ed28f commit 6170098

File tree

8 files changed

+309
-57
lines changed

8 files changed

+309
-57
lines changed

Cargo.lock

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/chat-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ tracing-subscriber = { version = "0.3.19", features = [
149149
"parking_lot",
150150
"time",
151151
] }
152+
typed-path = "0.11.0"
152153
unicode-width = "0.2.0"
153154
url = "2.5.4"
154155
uuid = { version = "1.15.1", features = ["v4", "serde"] }

crates/chat-cli/src/cli/chat/mcp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ fn resolve_scope_profile(ctx: &Context, scope: Option<Scope>) -> Result<PathBuf>
256256

257257
fn expand_path(ctx: &Context, p: &str) -> Result<PathBuf> {
258258
let p = shellexpand::tilde(p);
259-
let mut path = PathBuf::from(p.as_ref());
259+
let mut path = PathBuf::from(p.as_ref() as &str);
260260
if path.is_relative() {
261261
path = ctx.env().current_dir()?.join(path);
262262
}

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ impl McpServerConfig {
180180
let mut cwd = std::env::current_dir()?;
181181
cwd.push(".amazonq/mcp.json");
182182
let expanded_path = shellexpand::tilde("~/.aws/amazonq/mcp.json");
183-
let global_path = PathBuf::from(expanded_path.as_ref());
183+
let global_path = PathBuf::from(expanded_path.as_ref() as &str);
184184
let global_buf = tokio::fs::read(global_path).await.ok();
185185
let local_buf = tokio::fs::read(cwd).await.ok();
186186
let conf = match (global_buf, local_buf) {

crates/chat-cli/src/cli/chat/tools/fs_read.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ fn format_ftype(md: &Metadata) -> char {
497497
}
498498

499499
/// Formats a permissions mode into the form used by `ls`, e.g. `0o644` to `rw-r--r--`
500+
#[cfg(unix)]
500501
fn format_mode(mode: u32) -> [char; 9] {
501502
let mut mode = mode & 0o777;
502503
let mut res = ['-'; 9];
@@ -644,6 +645,7 @@ mod tests {
644645
}
645646

646647
#[test]
648+
#[cfg(unix)]
647649
fn test_format_mode() {
648650
macro_rules! assert_mode {
649651
($actual:expr, $expected:expr) => {

crates/chat-cli/src/platform/fs.rs renamed to crates/chat-cli/src/platform/fs/mod.rs

Lines changed: 98 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,70 @@ use std::sync::{
1313
use tempfile::TempDir;
1414
use tokio::fs;
1515

16+
// Import platform-specific modules
17+
#[cfg(unix)]
18+
mod unix;
19+
#[cfg(windows)]
20+
mod windows;
21+
22+
// Use platform-specific functions
23+
#[cfg(unix)]
24+
use unix::{
25+
append as platform_append,
26+
symlink_sync,
27+
};
28+
#[cfg(windows)]
29+
use windows::{
30+
append as platform_append,
31+
symlink_sync,
32+
};
33+
34+
/// Rust path handling is hard coded to work specific ways depending on the
35+
/// OS that is being executed on. Because of this, if Unix paths are provided,
36+
/// they aren't recognized. For example a leading prefix of '/' isn't considered
37+
/// an absolute path. To fix this, all test paths would need to have windows
38+
/// equivalents which is tedious and can lead to errors and missed test cases.
39+
/// To make writing tests easier, path normalization happens on Windows systems
40+
/// implicitly during test runtime.
41+
#[cfg(test)]
42+
fn normalize_test_path(path: impl AsRef<Path>) -> PathBuf {
43+
#[cfg(windows)]
44+
{
45+
use typed_path::Utf8TypedPath;
46+
let path_ref = path.as_ref();
47+
48+
// Only process string paths with forward slashes
49+
let typed_path = Utf8TypedPath::derive(path_ref.to_str().unwrap());
50+
if typed_path.is_unix() {
51+
let windows_path = typed_path.with_windows_encoding().to_string();
52+
53+
// If path is absolute (starts with /) and doesn't already have a drive letter
54+
if PathBuf::from(&windows_path).has_root() {
55+
// Prepend C: drive letter to make it truly absolute on Windows
56+
return PathBuf::from(format!("C:{}", windows_path));
57+
}
58+
59+
return PathBuf::from(windows_path);
60+
}
61+
}
62+
path.as_ref().to_path_buf()
63+
}
64+
65+
/// Cross-platform path append that handles test paths consistently
66+
fn append(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBuf {
67+
#[cfg(test)]
68+
{
69+
// Normalize the path for tests, then use the platform-specific append
70+
platform_append(normalize_test_path(base), normalize_test_path(path))
71+
}
72+
73+
#[cfg(not(test))]
74+
{
75+
// In non-test code, just use the platform-specific append directly
76+
platform_append(base, path)
77+
}
78+
}
79+
1680
#[derive(Debug, Clone, Default)]
1781
pub struct Fs(inner::Inner);
1882

@@ -285,29 +349,34 @@ impl Fs {
285349
/// Creates a new symbolic link on the filesystem.
286350
///
287351
/// The `link` path will be a symbolic link pointing to the `original` path.
288-
///
289-
/// This is a proxy to [`tokio::fs::symlink`].
290-
#[cfg(unix)]
291352
pub async fn symlink(&self, original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
292353
use inner::Inner;
354+
355+
#[cfg(unix)]
356+
async fn do_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
357+
fs::symlink(original, link).await
358+
}
359+
360+
#[cfg(windows)]
361+
async fn do_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
362+
windows::symlink_async(original, link).await
363+
}
364+
293365
match &self.0 {
294-
Inner::Real => fs::symlink(original, link).await,
295-
Inner::Chroot(root) => fs::symlink(append(root.path(), original), append(root.path(), link)).await,
366+
Inner::Real => do_symlink(original, link).await,
367+
Inner::Chroot(root) => do_symlink(append(root.path(), original), append(root.path(), link)).await,
296368
Inner::Fake(_) => panic!("unimplemented"),
297369
}
298370
}
299371

300372
/// Creates a new symbolic link on the filesystem.
301373
///
302374
/// The `link` path will be a symbolic link pointing to the `original` path.
303-
///
304-
/// This is a proxy to [`std::os::unix::fs::symlink`].
305-
#[cfg(unix)]
306375
pub fn symlink_sync(&self, original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
307376
use inner::Inner;
308377
match &self.0 {
309-
Inner::Real => std::os::unix::fs::symlink(original, link),
310-
Inner::Chroot(root) => std::os::unix::fs::symlink(append(root.path(), original), append(root.path(), link)),
378+
Inner::Real => symlink_sync(original, link),
379+
Inner::Chroot(root) => symlink_sync(append(root.path(), original), append(root.path(), link)),
311380
Inner::Fake(_) => panic!("unimplemented"),
312381
}
313382
}
@@ -403,35 +472,6 @@ impl Fs {
403472
}
404473
}
405474

406-
/// Performs `a.join(b)`, except:
407-
/// - if `b` is an absolute path, then the resulting path will equal `/a/b`
408-
/// - if the prefix of `b` contains some `n` copies of a, then the resulting path will equal `/a/b`
409-
#[cfg(unix)]
410-
fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
411-
use std::ffi::OsString;
412-
use std::os::unix::ffi::{
413-
OsStrExt,
414-
OsStringExt,
415-
};
416-
417-
// Have to use byte slices since rust seems to always append
418-
// a forward slash at the end of a path...
419-
let a = a.as_ref().as_os_str().as_bytes();
420-
let mut b = b.as_ref().as_os_str().as_bytes();
421-
while b.starts_with(a) {
422-
b = b.strip_prefix(a).unwrap();
423-
}
424-
while b.starts_with(b"/") {
425-
b = b.strip_prefix(b"/").unwrap();
426-
}
427-
PathBuf::from(OsString::from_vec(a.to_vec())).join(PathBuf::from(OsString::from_vec(b.to_vec())))
428-
}
429-
430-
#[cfg(windows)]
431-
fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
432-
todo!()
433-
}
434-
435475
#[cfg(test)]
436476
mod tests {
437477
use super::*;
@@ -466,18 +506,25 @@ mod tests {
466506
assert_eq!(fs.read_to_string(dir.path().join("write")).await.unwrap(), "write");
467507
}
468508

469-
#[test]
470-
fn test_append() {
471-
macro_rules! assert_append {
472-
($a:expr, $b:expr, $expected:expr) => {
473-
assert_eq!(append($a, $b), PathBuf::from($expected));
474-
};
475-
}
476-
assert_append!("/abc/test", "/test", "/abc/test/test");
477-
assert_append!("/tmp/.dir", "/tmp/.dir/home/myuser", "/tmp/.dir/home/myuser");
478-
assert_append!("/tmp/.dir", "/tmp/hello", "/tmp/.dir/tmp/hello");
479-
assert_append!("/tmp/.dir", "/tmp/.dir/tmp/.dir/home/user", "/tmp/.dir/home/user");
480-
}
509+
macro_rules! test_append_cases {
510+
($(
511+
$name:ident: ($a:expr, $b:expr) => $expected:expr
512+
),* $(,)?) => {
513+
$(
514+
#[test]
515+
fn $name() {
516+
assert_eq!(append($a, $b), normalize_test_path($expected));
517+
}
518+
)*
519+
};
520+
}
521+
522+
test_append_cases!(
523+
append_test_path_to_dir: ("/abc/test", "/test") => "/abc/test/test",
524+
append_absolute_to_tmp_dir: ("/tmp/.dir", "/tmp/.dir/home/myuser") => "/tmp/.dir/home/myuser",
525+
append_different_tmp_path: ("/tmp/.dir", "/tmp/hello") => "/tmp/.dir/tmp/hello",
526+
append_nested_path_to_tmpdir: ("/tmp/.dir", "/tmp/.dir/tmp/.dir/home/user") => "/tmp/.dir/home/user",
527+
);
481528

482529
#[tokio::test]
483530
async fn test_read_to_string() {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::ffi::OsString;
2+
use std::os::unix::ffi::{
3+
OsStrExt,
4+
OsStringExt,
5+
};
6+
use std::path::{
7+
Path,
8+
PathBuf,
9+
};
10+
11+
/// Performs `a.join(b)`, except:
12+
/// - if `b` is an absolute path, then the resulting path will equal `/a/b`
13+
/// - if the prefix of `b` contains some `n` copies of a, then the resulting path will equal `/a/b`
14+
pub(super) fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
15+
// Have to use byte slices since rust seems to always append
16+
// a forward slash at the end of a path...
17+
let a = a.as_ref().as_os_str().as_bytes();
18+
let mut b = b.as_ref().as_os_str().as_bytes();
19+
while b.starts_with(a) {
20+
b = b.strip_prefix(a).unwrap();
21+
}
22+
while b.starts_with(b"/") {
23+
b = b.strip_prefix(b"/").unwrap();
24+
}
25+
PathBuf::from(OsString::from_vec(a.to_vec())).join(PathBuf::from(OsString::from_vec(b.to_vec())))
26+
}
27+
28+
/// Creates a new symbolic link on the filesystem.
29+
///
30+
/// The `link` path will be a symbolic link pointing to the `original` path.
31+
pub(super) fn symlink_sync(original: impl AsRef<Path>, link: impl AsRef<Path>) -> std::io::Result<()> {
32+
std::os::unix::fs::symlink(original, link)
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use super::*;
38+
39+
#[test]
40+
fn test_append() {
41+
macro_rules! assert_append {
42+
($a:expr, $b:expr, $expected:expr) => {
43+
assert_eq!(append($a, $b), PathBuf::from($expected));
44+
};
45+
}
46+
assert_append!("/abc/test", "/test", "/abc/test/test");
47+
assert_append!("/tmp/.dir", "/tmp/.dir/home/myuser", "/tmp/.dir/home/myuser");
48+
assert_append!("/tmp/.dir", "/tmp/hello", "/tmp/.dir/tmp/hello");
49+
assert_append!("/tmp/.dir", "/tmp/.dir/tmp/.dir/home/user", "/tmp/.dir/home/user");
50+
}
51+
}

0 commit comments

Comments
 (0)