diff --git a/src/driver/mod.rs b/src/driver/mod.rs index c952b54f..9bddd2cb 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -389,15 +389,16 @@ pub(crate) mod tests { let lib_dir = canon(&ws.create_dir("workspace/libs/lib")); // Set up the dependency map for imports (e.g. `use lib::...`) - let mut map = DependencyMap::new(); - map.insert(workspace_dir.clone(), "lib".to_string(), lib_dir.clone()) - .expect("Failed to insert dependency map"); + let remappings = vec![crate::resolution::Remapping { + context_prefix: workspace_dir.clone(), + drp_name: "lib".to_string(), + target: lib_dir.clone(), + }]; + let mut map = DependencyMap::try_from(remappings).expect("Failed to create dependency map"); // Register the strict crate boundaries so local files are forced to use `crate::` - map.insert(workspace_dir.clone(), CRATE_STR.to_string(), workspace_dir) - .expect("Failed to insert workspace crate boundary"); - map.insert(lib_dir.clone(), CRATE_STR.to_string(), lib_dir) - .expect("Failed to insert library crate boundary"); + map.add_crate_root(workspace_dir); + map.add_crate_root(lib_dir); let map = Arc::new(map); let mut root_file_path = None; diff --git a/src/lib.rs b/src/lib.rs index 0f0d2e00..0f844026 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -494,31 +494,31 @@ pub(crate) mod tests { I: IntoIterator, K: Into, { - let mut dependency_map = DependencyMap::new(); + let mut remappings = Vec::new(); + let mut crate_roots = Vec::new(); if let Some(parent) = prog_path.as_ref().parent() { let canon_root = crate::resolution::tests::canon(parent); - let _ = dependency_map.insert( - canon_root.clone(), - crate::driver::CRATE_STR.to_string(), - canon_root, - ); + crate_roots.push(canon_root); } for (context, alias, target) in dependencies { let context = crate::resolution::tests::canon(context.as_ref()); let target = crate::resolution::tests::canon(target.as_ref()); - dependency_map - .insert(context.clone(), alias.into(), target.clone()) - .unwrap(); + remappings.push(crate::resolution::Remapping { + context_prefix: context, + drp_name: alias.into(), + target: target.clone(), + }); // Treat each mapped dependency as an isolated external package to satisfy strict local-file checks - let _ = dependency_map.insert( - target.clone(), - crate::driver::CRATE_STR.to_string(), - target, - ); + crate_roots.push(target); + } + + let mut dependency_map = DependencyMap::try_from(remappings).unwrap(); + for root in crate_roots { + dependency_map.add_crate_root(root); } TestCase::::template_deps(prog_path.as_ref(), &dependency_map) @@ -742,13 +742,7 @@ pub(crate) mod tests { let main_path = root.join("main.simf"); let mut dependency_map = DependencyMap::new(); let canon_root = CanonPath::canonicalize(&root).unwrap(); - dependency_map - .insert( - canon_root.clone(), - crate::driver::CRATE_STR.to_string(), - canon_root, - ) - .unwrap(); + dependency_map.add_crate_root(canon_root); TestCase::::template_deps(&main_path, &dependency_map) .with_arguments(Arguments::default()) diff --git a/src/main.rs b/src/main.rs index 8d6a15cc..31fcfad5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use base64::engine::general_purpose::STANDARD; use clap::{Arg, ArgAction, Command}; use simplicityhl::{ - driver::CRATE_STR, resolution::{CanonPath, DependencyMap, SourceFile}, AbiMeta, CompiledProgram, }; @@ -129,16 +128,13 @@ fn main() -> Result<(), Box> { .get_many::("dependencies") .unwrap_or_default(); - let mut dependencies = DependencyMap::new(); - - // Automatically assign the `crate` root to the project directory let canon_root = main_path .as_path() .parent() .and_then(|p| CanonPath::canonicalize(p).ok()); - if let Some(ref canon) = canon_root { - let _ = dependencies.insert(canon.clone(), CRATE_STR.to_string(), canon.clone()); - } + + let mut remappings = Vec::new(); + let mut target_paths = Vec::new(); for arg in dep_args { let (left_side, path_str) = arg.split_once('=').unwrap_or_else(|| { @@ -164,17 +160,31 @@ fn main() -> Result<(), Box> { let target_path = CanonPath::canonicalize(Path::new(path_str))?; - if let Err(e) = dependencies.insert(context_path, alias.to_string(), target_path.clone()) { - eprintln!("Error: {e}"); - std::process::exit(1); - } + remappings.push(simplicityhl::resolution::Remapping { + context_prefix: context_path, + drp_name: alias.to_string(), + target: target_path.clone(), + }); // Treat the external package as an isolated boundary, allowing it to use `crate::` internally - if let Err(e) = dependencies.insert(target_path.clone(), CRATE_STR.to_string(), target_path) - { + target_paths.push(target_path); + } + + let mut dependencies = match DependencyMap::try_from(remappings) { + Ok(map) => map, + Err(e) => { eprintln!("Error: {e}"); std::process::exit(1); } + }; + + // Automatically assign the `crate` root to the project directory + if let Some(ref canon) = canon_root { + dependencies.add_crate_root(canon.clone()); + } + + for target_path in target_paths { + dependencies.add_crate_root(target_path); } let source = SourceFile::new(main_path.as_path(), std::sync::Arc::from(main_text)); diff --git a/src/resolution.rs b/src/resolution.rs index 22ec3abb..7dfc1080 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -126,6 +126,18 @@ pub struct DependencyMap { inner: Vec, } +impl TryFrom> for DependencyMap { + type Error = io::Error; + + fn try_from(mappings: Vec) -> Result { + let mut map = DependencyMap::new(); + for m in mappings { + map.insert(m.context_prefix, m.drp_name, m.target)?; + } + Ok(map) + } +} + impl DependencyMap { pub fn new() -> Self { Self::default() @@ -135,6 +147,19 @@ impl DependencyMap { self.inner.is_empty() } + /// Safely sets the `crate` root for a given path. + /// + /// This configures a directory to act as an isolated package boundary, + /// enabling internal absolute imports via `crate::`. + pub fn add_crate_root(&mut self, root: CanonPath) { + self.inner.push(Remapping { + context_prefix: root.clone(), + drp_name: CRATE_STR.to_string(), + target: root, + }); + self.sort_mappings(); + } + /// Re-sort the vector in descending order so the longest context paths are always at the front. /// This mathematically guarantees that the first match we find is the most specific. fn sort_mappings(&mut self) { @@ -156,12 +181,19 @@ impl DependencyMap { /// programmer types in their source code (e.g., the `"math"` in `use math::vector;`). /// * `target` - The physical directory where the compiler should actually /// look for the code (e.g., `/libs/frontend_math`). - pub fn insert( + pub(crate) fn insert( &mut self, context: CanonPath, drp_name: String, target: CanonPath, ) -> io::Result<()> { + if drp_name == CRATE_STR { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("The '{}' keyword is reserved and cannot be manually mapped using `insert`. Use `add_crate_root` instead.", CRATE_STR), + )); + } + self.inner.push(Remapping { context_prefix: context, drp_name, @@ -304,6 +336,24 @@ pub(crate) mod tests { UseDecl::dummy_path(path) } + /// Attempting to manually map the `crate` keyword using `insert()` must result in an error. + #[test] + fn test_insert_crate_fails() { + let ws = TempWorkspace::new("insert_crate_fail"); + let project_dir = canon(&ws.create_dir("workspace")); + + let mappings = vec![Remapping { + context_prefix: project_dir.clone(), + drp_name: CRATE_STR.to_string(), + target: project_dir.clone(), + }]; + + let result = DependencyMap::try_from(mappings); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("reserved")); + } + /// When a user registers the same library dependency root path multiple times /// for different folders, the compiler must always check the longest folder path first. #[test] @@ -318,13 +368,24 @@ pub(crate) mod tests { let target_v3 = canon(&ws.create_dir("lib/math_v3")); let target_v2 = canon(&ws.create_dir("lib/math_v2")); - let mut map = DependencyMap::new(); - map.insert(workspace_dir.clone(), "math".to_string(), target_v1) - .unwrap(); - map.insert(nested_dir.clone(), "math".to_string(), target_v3) - .unwrap(); - map.insert(project_a_dir.clone(), "math".to_string(), target_v2) - .unwrap(); + let mappings = vec![ + Remapping { + context_prefix: workspace_dir.clone(), + drp_name: "math".to_string(), + target: target_v1, + }, + Remapping { + context_prefix: nested_dir.clone(), + drp_name: "math".to_string(), + target: target_v3, + }, + Remapping { + context_prefix: project_a_dir.clone(), + drp_name: "math".to_string(), + target: target_v2, + }, + ]; + let map = DependencyMap::try_from(mappings).unwrap(); // The longest prefixes should bubble to the top assert_eq!(map.inner[0].context_prefix, nested_dir); @@ -342,9 +403,12 @@ pub(crate) mod tests { let target_utils = canon(&ws.create_dir("libs/utils_a")); let current_file = canon(&ws.create_file("project_b/main.simf", "")); - let mut map = DependencyMap::new(); - map.insert(project_a, "utils".to_string(), target_utils) - .unwrap(); + let mappings = vec![Remapping { + context_prefix: project_a, + drp_name: "utils".to_string(), + target: target_utils, + }]; + let map = DependencyMap::try_from(mappings).unwrap(); let use_decl = create_dummy_use_decl(&["utils"]); let result = map.resolve_path(¤t_file, &use_decl); @@ -372,11 +436,19 @@ pub(crate) mod tests { let frontend_target = canon(&ws.create_dir("libs/frontend_math")); let frontend_expected = canon(&ws.create_file("libs/frontend_math/vector.simf", "")); - let mut map = DependencyMap::new(); - map.insert(global_context, "math".to_string(), global_target) - .unwrap(); - map.insert(frontend_context, "math".to_string(), frontend_target) - .unwrap(); + let mappings = vec![ + Remapping { + context_prefix: global_context, + drp_name: "math".to_string(), + target: global_target, + }, + Remapping { + context_prefix: frontend_context, + drp_name: "math".to_string(), + target: frontend_target, + }, + ]; + let map = DependencyMap::try_from(mappings).unwrap(); let use_decl = create_dummy_use_decl(&["math", "vector"]); @@ -400,21 +472,13 @@ pub(crate) mod tests { ws.create_file("workspace/utils.simf", ""); let current_file = canon(&ws.create_file("workspace/main.simf", "")); - let mut map = DependencyMap::new(); - // The driver sets up the crate root - map.insert( - project_dir.clone(), - CRATE_STR.to_string(), - project_dir.clone(), - ) - .unwrap(); - // The user tries to alias a folder inside their own project as an external dependency - map.insert( - project_dir.clone(), - "utils_lib".to_string(), - project_dir.clone(), - ) - .unwrap(); + let mappings = vec![Remapping { + context_prefix: project_dir.clone(), + drp_name: "utils_lib".to_string(), + target: project_dir.clone(), + }]; + let mut map = DependencyMap::try_from(mappings).unwrap(); + map.add_crate_root(project_dir.clone()); let use_decl = create_dummy_use_decl(&["utils_lib", "utils"]); let result = map.resolve_path(¤t_file, &use_decl); @@ -436,12 +500,7 @@ pub(crate) mod tests { let current_file = canon(&ws.create_file("workspace/main.simf", "")); let mut map = DependencyMap::new(); - map.insert( - project_dir.clone(), - CRATE_STR.to_string(), - project_dir.clone(), - ) - .unwrap(); + map.add_crate_root(project_dir.clone()); let use_decl = create_dummy_use_decl(&[CRATE_STR, "utils"]); let result = map.resolve_path(¤t_file, &use_decl).unwrap(); @@ -480,8 +539,12 @@ pub(crate) mod tests { let current_file = canon(&ws.create_file("workspace/frontend/src/main.simf", "")); - let mut map = DependencyMap::new(); - map.insert(context, "math".to_string(), target).unwrap(); + let mappings = vec![Remapping { + context_prefix: context, + drp_name: "math".to_string(), + target, + }]; + let map = DependencyMap::try_from(mappings).unwrap(); let use_decl = create_dummy_use_decl(&["math", "vector"]); let result = map.resolve_path(¤t_file, &use_decl).unwrap(); diff --git a/tests/cli.rs b/tests/cli.rs index 2244e377..6ede40b4 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -27,3 +27,32 @@ fn cli_dependency_can_use_crate_root() { String::from_utf8_lossy(&output.stderr), ); } + +#[test] +fn cli_reserved_crate_mapping_fails() { + let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate"); + let main = root.join("main.simf"); + let ext_lib = root.join("ext_lib"); + + // Attempt to maliciously override the `crate` keyword + let dep_arg = format!("crate={}", ext_lib.display()); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(main) + .arg("--dep") + .arg(dep_arg) + .output() + .expect("failed to run simc"); + + assert!( + !output.status.success(), + "simc unexpectedly succeeded when overriding the 'crate' dependency" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("keyword is reserved"), + "Expected 'keyword is reserved' error, got:\n{}", + stderr + ); +}