From d90d08e628c211440d520a5bc5cb12b1d4069c41 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Mon, 20 Jul 2026 13:22:32 -0700 Subject: [PATCH 1/3] keep name as name in the installinator document --- artifact/src/installinator.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/artifact/src/installinator.rs b/artifact/src/installinator.rs index e65e9c3..60317ee 100644 --- a/artifact/src/installinator.rs +++ b/artifact/src/installinator.rs @@ -43,8 +43,6 @@ use crate::ArtifactVersion; /// own backwards compatibility code, which some temporary code in this module /// supports: /// -/// - `name` is accepted as an alias for [`InstallinatorArtifact::file_name`]. -/// We renamed this field in Tufaceous v2 for clarity. /// - `ControlPlane` is an accepted variant for [`InstallinatorArtifactKind`]. /// This indicates to Installinator that an older version of Wicket was used /// to read the repository, and that it will need to fetch the composite @@ -82,8 +80,11 @@ pub struct InstallinatorArtifact { pub hash: ArtifactHash, /// A file name without directory separators; not necessarily the target /// name. - // (temporary alias; see "v1 compatibility notes" above) - #[serde(alias = "name")] + /// + /// This is named `name` in the serialized document in order to retain + /// compatibility with older versions of Installinator, allowing us to + /// downgrade clean-slated development environments with mupdate. + #[serde(rename = "name")] pub file_name: String, } From d24aec34fcbdf5fda0db8103a1e77926bd166a86 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Mon, 20 Jul 2026 16:57:50 -0700 Subject: [PATCH 2/3] provide limited access to v1 artifacts for wicket --- lib/src/repo.rs | 77 ++++++++++++++++++++++++++++++++--- lib/src/repo/v1.rs | 22 ++++++++++ lib/tests/v1_compatibility.rs | 67 +++++++++++++++++++++++++++--- 3 files changed, 154 insertions(+), 12 deletions(-) diff --git a/lib/src/repo.rs b/lib/src/repo.rs index 9ad6675..0b93749 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -27,6 +27,7 @@ use tough::schema::Target; use tufaceous_artifact::Artifact; use tufaceous_artifact::ArtifactHash; use tufaceous_artifact::ArtifactSet; +use tufaceous_artifact::ArtifactVersion; use tufaceous_artifact::KnownArtifactTags; use tufaceous_artifact::Metadata; use tufaceous_artifact::artifact_set::GetError; @@ -52,6 +53,15 @@ pub struct Repository { artifact_data: BTreeMap, metadata: BTreeMap, + /// If this is a v1 repo, this contains the hash of the original + /// Installinator document (instead of the converted-to-v2 document). + installinator_v1_document: Option, + /// If this is a v1 repo, this contains artifacts that allow reading + /// the original Installinator document (instead of the converted-to-v2 + /// document), as well as the original control plane tarball referenced by + /// that document. + installinator_v1_artifacts: Vec, + // These are set directly by the ZIP archive convenience methods in the // loader module. // TODO after v2 merge: Move the loader module under this repo module so @@ -126,6 +136,10 @@ impl Repository { artifacts: partial.artifacts, artifact_data: partial.artifact_data, metadata: BTreeMap::new(), + installinator_v1_document: partial + .installinator_v1_document, + installinator_v1_artifacts: partial + .installinator_v1_artifacts, archive_path: None, archive_sha256: None, }); @@ -156,6 +170,8 @@ impl Repository { artifacts, artifact_data, metadata, + installinator_v1_document: None, + installinator_v1_artifacts: Vec::new(), archive_path: None, archive_sha256: None, }) @@ -290,7 +306,11 @@ impl Repository { tags: &KnownArtifactTags, ) -> Result { let artifact = self.artifacts.get_only(tags)?.clone(); - Ok(ArtifactHandle { artifact, repo: Arc::clone(self) }) + Ok(ArtifactHandle { + artifact, + repo: Arc::clone(self), + installinator_v1_target_name: None, + }) } /// Returns an iterator of [`ArtifactHandle`]s for every artifact @@ -300,10 +320,38 @@ impl Repository { /// together in a struct as a convenience. The repository must be wrapped in /// [`Arc`] to call this method. pub fn handles(self: &Arc) -> impl Iterator { - self.artifacts - .iter() - .cloned() - .map(|artifact| ArtifactHandle { artifact, repo: Arc::clone(self) }) + self.artifacts.iter().cloned().map(|artifact| ArtifactHandle { + artifact, + repo: Arc::clone(self), + installinator_v1_target_name: None, + }) + } + + /// If this was a v1 repository, returns the hash of the original + /// Installinator document (instead of the converted-to-v2 document). + pub fn installinator_v1_document(&self) -> Option { + self.installinator_v1_document.as_ref().copied() + } + + /// If this was a v1 repository, returns an iterator of [`ArtifactHandle`] + /// that can be used to read the original Installinator document or control + /// plane tarball. + /// + /// Note that the `Artifact` returned by the handle has no tags as it is not + /// a valid artifact. + pub fn installinator_v1_handles( + self: &Arc, + ) -> impl Iterator { + self.installinator_v1_artifacts.iter().map(|artifact| ArtifactHandle { + artifact: Artifact { + version: artifact.version.clone(), + tags: BTreeMap::new(), + hash: artifact.hash, + length: artifact.length, + }, + repo: Arc::clone(self), + installinator_v1_target_name: Some(artifact.target_name.clone()), + }) } /// Reads all targets in the repository and verifies they have the correct @@ -396,6 +444,14 @@ impl ArtifactData { } } +#[derive(Debug, Clone)] +struct InstallinatorV1Artifact { + version: ArtifactVersion, + hash: ArtifactHash, + length: u64, + target_name: String, +} + /// An [`Artifact`] and the [`Repository`] it belongs to, for convenience to /// code that works at the artifact level but needs to read the artifact data /// from the repository. @@ -405,6 +461,11 @@ impl ArtifactData { pub struct ArtifactHandle { artifact: Artifact, repo: Arc, + + /// If `Some`, this is not a "real" artifact but instead was generated + /// by [`Repository::installinator_v1_artifacts`], and `artifact.tags` is + /// empty. + installinator_v1_target_name: Option, } impl ArtifactHandle { @@ -421,7 +482,11 @@ impl ArtifactHandle { /// Read this artifact from its repository. pub async fn stream(&self) -> Result { - self.repo.read_artifact(&self.artifact).await + if let Some(target_name) = &self.installinator_v1_target_name { + self.repo.read_target(target_name).await + } else { + self.repo.read_artifact(&self.artifact).await + } } } diff --git a/lib/src/repo/v1.rs b/lib/src/repo/v1.rs index c950a44..66d8923 100644 --- a/lib/src/repo/v1.rs +++ b/lib/src/repo/v1.rs @@ -56,6 +56,7 @@ use crate::error::Error; use crate::error::ErrorKind; use crate::error::try_path; use crate::repo::ArtifactData; +use crate::repo::InstallinatorV1Artifact; use crate::repo::read_target; use crate::repo::read_target_json; use crate::repo::read_target_vec; @@ -66,6 +67,8 @@ pub(super) struct PartialRepository { pub(super) system_version: Version, pub(super) artifacts: ArtifactSet, pub(super) artifact_data: BTreeMap, + pub(super) installinator_v1_document: Option, + pub(super) installinator_v1_artifacts: Vec, } impl PartialRepository { @@ -98,6 +101,8 @@ pub(crate) async fn from_loaded( system_version, artifacts: ArtifactSet::default(), artifact_data: BTreeMap::new(), + installinator_v1_document: None, + installinator_v1_artifacts: Vec::new(), }; let mut installinator_document = None; for V1Artifact { version, kind, target } in v1_artifacts { @@ -214,6 +219,14 @@ pub(crate) async fn from_loaded( } V1KnownArtifactKind::ControlPlane => { + partial.installinator_v1_artifacts.push( + InstallinatorV1Artifact { + version, + hash, + length, + target_name: target.clone(), + }, + ); CompositeArtifact::unpack(tuf_repo, target) .await? .read_control_plane(&mut partial) @@ -226,6 +239,15 @@ pub(crate) async fn from_loaded( // for the v2 artifacts once all of the potential artifacts are // extracted. V1KnownArtifactKind::InstallinatorDocument => { + partial.installinator_v1_document = Some(hash); + partial.installinator_v1_artifacts.push( + InstallinatorV1Artifact { + version: version.clone(), + hash, + length, + target_name: target.clone(), + }, + ); installinator_document = Some((version, target)); continue; } diff --git a/lib/tests/v1_compatibility.rs b/lib/tests/v1_compatibility.rs index 7d612ee..86ce841 100644 --- a/lib/tests/v1_compatibility.rs +++ b/lib/tests/v1_compatibility.rs @@ -2,11 +2,16 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::collections::BTreeSet; +use std::sync::Arc; + use camino::Utf8Path; +use futures_util::TryStreamExt; use tufaceous::ExpirationEnforcement; use tufaceous::RepositoryLoader; use tufaceous::error::Error; use tufaceous_artifact::ArtifactSet; +use tufaceous_artifact::InstallinatorDocument; use tufaceous_artifact::KnownArtifactTags; use tufaceous_artifact::OsBoard; use tufaceous_artifact::OsPhase1Tags; @@ -25,12 +30,14 @@ async fn v1_fake() -> Result<(), Error> { let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/data/v1-fake.zip"); - let repo = RepositoryLoader::new() - .expiration_enforcement(ExpirationEnforcement::Unsafe) - .unsafe_blindly_trust_repo() - .v1_compatibility(true) - .load_zip_path(path, &log) - .await?; + let repo = Arc::new( + RepositoryLoader::new() + .expiration_enforcement(ExpirationEnforcement::Unsafe) + .unsafe_blindly_trust_repo() + .v1_compatibility(true) + .load_zip_path(path, &log) + .await?, + ); let parallelism = std::thread::available_parallelism().unwrap().get(); repo.verify_targets(parallelism).await?; @@ -77,6 +84,54 @@ async fn v1_fake() -> Result<(), Error> { // And there should be no unexpected artifacts: assert_eq!(&seen, repo.artifacts()); + // The Installinator document should be regenerated to only reference + // v2 artifacts. + let mut hashes = repo + .artifacts() + .iter() + .map(|artifact| artifact.hash) + .collect::>(); + let artifact = repo + .artifacts() + .get_only(&KnownArtifactTags::InstallinatorDocument) + .unwrap(); + let doc_json = repo + .read_artifact(&artifact) + .await? + .map_ok(Vec::from) + .try_concat() + .await?; + let doc: InstallinatorDocument = serde_json::from_slice(&doc_json).unwrap(); + for artifact in doc.artifacts { + assert!( + hashes.contains(&artifact.hash), + "converted Installinator document references non-existent hash {}", + artifact.hash + ); + } + + // The original Installinator document should be accessible, and may + // reference either v2 or v1-only artifacts. + hashes.extend( + repo.installinator_v1_handles().map(|handle| handle.artifact().hash), + ); + let handle = repo + .installinator_v1_handles() + .find(|handle| { + handle.artifact().hash == repo.installinator_v1_document().unwrap() + }) + .unwrap(); + let doc_json = + handle.stream().await?.map_ok(Vec::from).try_concat().await?; + let doc: InstallinatorDocument = serde_json::from_slice(&doc_json).unwrap(); + for artifact in doc.artifacts { + assert!( + hashes.contains(&artifact.hash), + "original Installinator document references non-existent hash {}", + artifact.hash + ); + } + Ok(()) } From ab5678d7b9534d40a3aa9c2de4e6b1152f9fb9b0 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Mon, 20 Jul 2026 20:55:46 -0700 Subject: [PATCH 3/3] hush, clippy --- lib/tests/v1_compatibility.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tests/v1_compatibility.rs b/lib/tests/v1_compatibility.rs index 86ce841..02119ba 100644 --- a/lib/tests/v1_compatibility.rs +++ b/lib/tests/v1_compatibility.rs @@ -96,7 +96,7 @@ async fn v1_fake() -> Result<(), Error> { .get_only(&KnownArtifactTags::InstallinatorDocument) .unwrap(); let doc_json = repo - .read_artifact(&artifact) + .read_artifact(artifact) .await? .map_ok(Vec::from) .try_concat()