From b02437fc467e238b32407351aae5d3657dcb1650 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 27 Sep 2026 00:37:07 -0700 Subject: [PATCH 1/2] fix(core): resolve explicit PlatformIO registry type paths --- crates/fbuild-core/src/platformio_package.rs | 51 +++++++++++++++++-- .../tests/platformio_package_resolution.rs | 30 +++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/fbuild-core/src/platformio_package.rs b/crates/fbuild-core/src/platformio_package.rs index ebbe5c0b..593f4b86 100644 --- a/crates/fbuild-core/src/platformio_package.rs +++ b/crates/fbuild-core/src/platformio_package.rs @@ -88,6 +88,10 @@ pub struct RegistrySpec { pub owner: Option, pub name: String, pub requirement: Option, + /// Explicit PlatformIO registry category from an `owner/type/name` or + /// `type/owner/name` path. Framework/toolchain aliases both use `tool`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub registry_type: Option, } /// A source accepted by PlatformIO's package-spec grammar. An archive URL or @@ -286,9 +290,22 @@ pub fn parse_package_spec(raw: &str) -> Result { } fn parse_registry(name: &str, requirement: Option<&str>) -> Result { - let (owner, name) = match name.split_once('/') { - Some((owner, name)) => (Some(owner), name), - None => (None, name), + let parts = name.split('/').collect::>(); + let (owner, name, registry_type) = match parts.as_slice() { + [name] => (None, *name, None), + [owner, name] => (Some(*owner), *name, None), + [first, middle, name] => { + let first_type = canonical_registry_type(first); + let middle_type = canonical_registry_type(middle); + match (first_type, middle_type) { + (Some(kind), None) => (Some(*middle), *name, Some(kind)), + // Owner-first is canonical when both segments look like types: + // registry owner names are not reserved words. + (_, Some(kind)) => (Some(*first), *name, Some(kind)), + _ => return Err(ResolutionError::InvalidSpec(name.to_string())), + } + } + _ => return Err(ResolutionError::InvalidSpec(name.to_string())), }; if !valid_segment(name) || owner.is_some_and(|o| !valid_segment(o)) { return Err(ResolutionError::InvalidSpec(name.into())); @@ -300,9 +317,19 @@ fn parse_registry(name: &str, requirement: Option<&str>) -> Result owner: owner.map(str::to_string), name: name.into(), requirement: requirement.map(str::to_string), + registry_type: registry_type.map(str::to_string), }) } +fn canonical_registry_type(raw: &str) -> Option<&'static str> { + match raw { + "platform" => Some("platform"), + "tool" | "toolchain" | "framework" | "uploader" | "debugger" => Some("tool"), + "library" => Some("library"), + _ => None, + } +} + fn valid_segment(value: &str) -> bool { !matches!(value, "" | "." | "..") && value @@ -439,6 +466,7 @@ fn parse_direct_source(value: &str) -> Result { /// Construct the metadata endpoint for an owner-qualified registry name. pub fn registry_api_url(kind: PackageKind, spec: &RegistrySpec) -> Result { + validate_registry_type(kind, spec)?; let owner = spec .owner .as_deref() @@ -456,6 +484,22 @@ pub fn registry_api_url(kind: PackageKind, spec: &RegistrySpec) -> Result Result<()> { + if spec + .registry_type + .as_deref() + .is_some_and(|declared| declared != kind.registry_type()) + { + return Err(ResolutionError::InvalidSpec(format!( + "{} is a {} package, not a {} package", + spec.name, + spec.registry_type.as_deref().unwrap_or_default(), + kind.registry_type() + ))); + } + Ok(()) +} + #[derive(Deserialize)] struct RegistryResponse { name: String, @@ -534,6 +578,7 @@ pub fn resolve_registry_json( system: &str, metadata_json: &str, ) -> Result { + validate_registry_type(kind, spec)?; if let Some(requirement) = &spec.requirement { validate_requirement(requirement)?; } diff --git a/crates/fbuild-core/tests/platformio_package_resolution.rs b/crates/fbuild-core/tests/platformio_package_resolution.rs index d8dbcd4e..594401d1 100644 --- a/crates/fbuild-core/tests/platformio_package_resolution.rs +++ b/crates/fbuild-core/tests/platformio_package_resolution.rs @@ -34,6 +34,36 @@ fn parses_generic_registry_aliases_without_platform_dispatch() { ); } +#[test] +fn explicit_registry_type_paths_normalize_without_native_dispatch() { + let owner_first = + parse_package_spec("platformio/tool/toolchain-gccarmnoneeabi@1.90201.191206").unwrap(); + let type_first = + parse_package_spec("tool/platformio/toolchain-gccarmnoneeabi@1.90201.191206").unwrap(); + let ordinary = + parse_package_spec("platformio/toolchain-gccarmnoneeabi@1.90201.191206").unwrap(); + assert_eq!(owner_first.registry(), type_first.registry()); + assert_eq!( + owner_first.registry().unwrap().name, + ordinary.registry().unwrap().name + ); + assert_eq!( + registry_api_url(PackageKind::Tool, owner_first.registry().unwrap()).unwrap(), + "https://api.registry.platformio.org/v3/packages/platformio/tool/toolchain-gccarmnoneeabi" + ); + assert!(registry_api_url(PackageKind::Platform, owner_first.registry().unwrap()).is_err()); + assert!(parse_package_spec("platformio/unknown/toolchain-gccarmnoneeabi@1.0.0").is_err()); + let type_named_owner = parse_package_spec("tool/library/example@1.0.0").unwrap(); + assert_eq!( + type_named_owner.registry().unwrap().owner.as_deref(), + Some("tool") + ); + assert_eq!( + registry_api_url(PackageKind::Library, type_named_owner.registry().unwrap()).unwrap(), + "https://api.registry.platformio.org/v3/packages/tool/library/example" + ); +} + #[test] fn classifies_archive_repository_and_local_payload_paths() { let archive = parse_package_spec("https://example.test/platform.zip").unwrap(); From 674359a760c8df67f09821c06d796d0a8ae7449b Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 27 Sep 2026 00:38:36 -0700 Subject: [PATCH 2/2] chore(ci): refresh platform-boundary inventory --- ci/platform_boundary_research.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/platform_boundary_research.tsv b/ci/platform_boundary_research.tsv index ce9ed91d..bc17cd26 100644 --- a/ci/platform_boundary_research.tsv +++ b/ci/platform_boundary_research.tsv @@ -67,7 +67,7 @@ crates/fbuild-core/src/platform/windows/usb_pnp.rs 35 native_path windows_sys:: crates/fbuild-core/src/platform/windows/usb_pnp.rs 39 native_path windows_sys:: process host_mechanic crates/fbuild-core/src/platform/windows/usb_pnp.rs 40 native_path windows_sys:: process host_mechanic crates/fbuild-core/tests/platformio_package_resolution.rs 5 attr_cfg #[cfg(windows)] host host_mechanic -crates/fbuild-core/tests/platformio_package_resolution.rs 76 attr_cfg #[cfg(windows)] host host_mechanic +crates/fbuild-core/tests/platformio_package_resolution.rs 106 attr_cfg #[cfg(windows)] host host_mechanic crates/fbuild-daemon/Cargo.toml 68 native_dependency libc process host_mechanic crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs 146 attr_cfg #[cfg(windows)] host_executable host_artifact_policy crates/fbuild-daemon/src/handlers/emulator/tests_process.rs 9 attr_cfg #[cfg(windows)] host_executable host_artifact_policy