Repository: Aleph-Alpha/ts-rs
Pull request #247 (2024-02-29 18:59:04)

bug: `#[ts(export_to = "../path")]` can cause `diff_paths` to fail
This PR reproduces the potential for paths containing `..` to break `diff_paths`


Patch:
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index 41e96c5..eeb6078 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -1,3 +1,5 @@
+mod path;
+
 use std::{
     any::TypeId,
     collections::BTreeMap,
@@ -7,7 +9,6 @@ use std::{
 };
 
 use thiserror::Error;
-use ExportError::*;
 
 use crate::TS;
 
@@ -107,8 +108,8 @@ pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
         use dprint_plugin_typescript::{configuration::ConfigurationBuilder, format_text};
 
         let fmt_cfg = ConfigurationBuilder::new().deno().build();
-        if let Some(formatted) =
-            format_text(path.as_ref(), &buffer, &fmt_cfg).map_err(|e| Formatting(e.to_string()))?
+        if let Some(formatted) = format_text(path.as_ref(), &buffer, &fmt_cfg)
+            .map_err(|e| ExportError::Formatting(e.to_string()))?
         {
             buffer = formatted;
         }
@@ -158,11 +159,11 @@ pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String
 
 /// Compute the output path to where `T` should be exported.
 fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
-    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| ManifestDirNotSet)?;
-    let manifest_dir = Path::new(&manifest_dir);
-    let path =
-        PathBuf::from(T::get_export_to().ok_or(CannotBeExported(std::any::type_name::<T>()))?);
-    Ok(manifest_dir.join(path))
+    path::absolute(Path::new(
+        &T::get_export_to()
+            .ok_or_else(|| std::any::type_name::<T>())
+            .map_err(ExportError::CannotBeExported)?,
+    ))
 }
 
 /// Push the declaration of `T`
@@ -180,7 +181,8 @@ fn generate_decl<T: TS + ?Sized>(out: &mut String) {
 
 /// Push an import statement for all dependencies of `T`
 fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
-    let export_to = T::get_export_to().ok_or(CannotBeExported(std::any::type_name::<T>()))?;
+    let export_to =
+        T::get_export_to().ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
     let path = Path::new(&export_to);
 
     let deps = T::dependencies();
@@ -233,47 +235,46 @@ fn import_path(from: &Path, import: &Path) -> String {
 //
 // Adapted from rustc's path_relative_from
 // https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158
-fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
+fn diff_paths<P, B>(path: P, base: B) -> Result<PathBuf, ExportError>
 where
     P: AsRef<Path>,
     B: AsRef<Path>,
 {
-    let path = path.as_ref();
-    let base = base.as_ref();
+    use Component as C;
 
-    if path.is_absolute() != base.is_absolute() {
-        if path.is_absolute() {
-            Some(PathBuf::from(path))
-        } else {
-            None
-        }
-    } else {
-        let mut ita = path.components();
-        let mut itb = base.components();
-        let mut comps: Vec<Component> = vec![];
-        loop {
-            match (ita.next(), itb.next()) {
-                (None, None) => break,
-                (Some(a), None) => {
-                    comps.push(a);
-                    comps.extend(ita.by_ref());
-                    break;
-                }
-                (None, _) => comps.push(Component::ParentDir),
-                (Some(a), Some(b)) if comps.is_empty() && a == b => (),
-                (Some(a), Some(Component::CurDir)) => comps.push(a),
-                (Some(_), Some(Component::ParentDir)) => return None,
-                (Some(a), Some(_)) => {
+    let path = path::absolute(path)?;
+    let base = path::absolute(base)?;
+
+    let mut ita = path.components();
+    let mut itb = base.components();
+    let mut comps: Vec<Component> = vec![];
+
+    loop {
+        match (ita.next(), itb.next()) {
+            (Some(C::ParentDir | C::CurDir), _) | (_, Some(C::ParentDir | C::CurDir)) => {
+                unreachable!(
+                    "The paths have been cleaned, no no '.' or '..' components are present"
+                )
+            }
+            (None, None) => break,
+            (Some(a), None) => {
+                comps.push(a);
+                comps.extend(ita.by_ref());
+                break;
+            }
+            (None, _) => comps.push(Component::ParentDir),
+            (Some(a), Some(b)) if comps.is_empty() && a == b => (),
+            (Some(a), Some(_)) => {
+                comps.push(Component::ParentDir);
+                for _ in itb {
                     comps.push(Component::ParentDir);
-                    for _ in itb {
-                        comps.push(Component::ParentDir);
-                    }
-                    comps.push(a);
-                    comps.extend(ita.by_ref());
-                    break;
                 }
+                comps.push(a);
+                comps.extend(ita.by_ref());
+                break;
             }
         }
-        Some(comps.iter().map(|c| c.as_os_str()).collect())
     }
+
+    Ok(comps.iter().map(|c| c.as_os_str()).collect())
 }
diff --git a/ts-rs/src/export/path.rs b/ts-rs/src/export/path.rs
new file mode 100644
index 0000000..be70483
--- /dev/null
+++ b/ts-rs/src/export/path.rs
@@ -0,0 +1,31 @@
+use std::path::{Component as C, Path, PathBuf};
+
+use super::ExportError as E;
+
+const ERROR_MESSAGE: &str = r#"The path provided with `#[ts(export_to = "..")]` is not valid"#;
+pub fn absolute<T: AsRef<Path>>(path: T) -> Result<PathBuf, E> {
+    let path = path.as_ref();
+
+    if path.is_absolute() {
+        return Ok(path.to_owned());
+    }
+
+    let path = std::env::current_dir()?.join(path);
+
+    let mut out = Vec::new();
+    for comp in path.components() {
+        match comp {
+            C::CurDir => (),
+            C::ParentDir => {
+                out.pop().ok_or(E::CannotBeExported(ERROR_MESSAGE))?;
+            }
+            comp => out.push(comp),
+        }
+    }
+
+    Ok(if !out.is_empty() {
+        out.iter().collect()
+    } else {
+        PathBuf::from(".")
+    })
+}
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 2540d62..4f99ed9 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -562,10 +562,7 @@ impl<T: TS, E: TS> TS for Result<T, E> {
     where
         Self: 'static,
     {
-        T::generics()
-            .push::<T>()
-            .extend(E::generics())
-            .push::<E>()
+        T::generics().push::<T>().extend(E::generics()).push::<E>()
     }
 }
 
@@ -658,10 +655,7 @@ impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
     where
         Self: 'static,
     {
-        K::generics()
-            .push::<K>()
-            .extend(V::generics())
-            .push::<V>()
+        K::generics().push::<K>().extend(V::generics()).push::<V>()
     }
 }
 


Test patch:
diff --git a/ts-rs/tests/path_bug.rs b/ts-rs/tests/path_bug.rs
new file mode 100644
index 0000000..cadfe15
--- /dev/null
+++ b/ts-rs/tests/path_bug.rs
@@ -0,0 +1,24 @@
+#![allow(dead_code)]
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(export, export_to = "../ts-rs/tests-out/path_bug/")]
+struct Foo {
+    bar: Bar,
+}
+
+#[derive(TS)]
+#[ts(export_to = "tests-out/path_bug/aaa/")]
+struct Bar {
+    i: i32,
+}
+
+#[test]
+fn path_bug() {
+    Foo::export().unwrap();
+    Bar::export().unwrap();
+
+    let base = std::env::current_dir().unwrap();
+    assert!(base.join("./tests-out/path_bug/Foo.ts").is_file());
+    assert!(base.join("./tests-out/path_bug/aaa/Bar.ts").is_file());
+}
\ No newline at end of file
diff --git a/ts-rs/tests/struct_tag.rs b/ts-rs/tests/struct_tag.rs
index 752a325..a1d8dd2 100644
--- a/ts-rs/tests/struct_tag.rs
+++ b/ts-rs/tests/struct_tag.rs
@@ -20,4 +20,3 @@ fn test() {
         "{ type: \"TaggedType\", a: number, b: number, }"
     )
 }
-