1use std::io::Cursor;
9
10use crate::bundle::{
11 ArtifactBlobConfig, ArtifactBundleManifest, STOW_BUNDLE_MANIFEST_PATH, STOW_DYLIB_MEDIA_TYPE,
12 STOW_PROC_MACRO_MEDIA_TYPE, STOW_RLIB_MEDIA_TYPE, STOW_RMETA_MEDIA_TYPE,
13};
14use crate::public_cache::stable_c_metadata_for_compile_key;
15
16#[derive(Debug, thiserror::Error)]
18#[error("invalid bundle: {0}")]
19pub struct BundleSchemaError(pub String);
20
21pub fn validate_bundle_schema(bytes: &[u8]) -> Result<(), BundleSchemaError> {
28 let mut archive = tar::Archive::new(Cursor::new(bytes));
29 let mut manifest_bytes = None::<Vec<u8>>;
30 for entry in archive
31 .entries()
32 .map_err(|error| BundleSchemaError(format!("read bundle entries: {error}")))?
33 {
34 let mut entry =
35 entry.map_err(|error| BundleSchemaError(format!("read bundle entry: {error}")))?;
36 let path = entry
37 .path()
38 .map_err(|error| BundleSchemaError(format!("read bundle path: {error}")))?
39 .to_string_lossy()
40 .to_string();
41 if path != STOW_BUNDLE_MANIFEST_PATH {
42 continue;
43 }
44 let mut bytes = Vec::new();
45 std::io::Read::read_to_end(&mut entry, &mut bytes)
46 .map_err(|error| BundleSchemaError(format!("read bundle manifest payload: {error}")))?;
47 manifest_bytes = Some(bytes);
48 break;
49 }
50 let manifest_bytes = manifest_bytes
51 .ok_or_else(|| BundleSchemaError("bundle is missing manifest.json".to_owned()))?;
52 let manifest = serde_json::from_slice::<ArtifactBundleManifest>(&manifest_bytes)
53 .map_err(|error| BundleSchemaError(format!("parse bundle manifest json: {error}")))?;
54 validate_bundle_config_identity(&manifest.config)?;
55 Ok(())
56}
57
58fn validate_bundle_config_identity(config: &ArtifactBlobConfig) -> Result<(), BundleSchemaError> {
59 let stable_c_metadata =
60 stable_c_metadata_for_compile_key(&config.compile_key).map_err(|error| {
61 BundleSchemaError(format!(
62 "bundle compile_key {} is not a valid stable public-cache identity: {error}",
63 config.compile_key
64 ))
65 })?;
66 if stable_c_metadata != config.c_metadata.as_str() {
67 return Err(BundleSchemaError(format!(
68 "bundle c_metadata {} does not match stable compile_key prefix {}",
69 config.c_metadata, stable_c_metadata
70 )));
71 }
72
73 let canonical_crate_name = config.crate_name.as_str().replace('-', "_");
74 let canonical_stem = format!(
75 "lib{}{extra}",
76 canonical_crate_name,
77 extra = config.extra_filename
78 );
79 let mut saw_canonical_rlib = false;
80 let mut saw_canonical_rmeta = false;
81 let mut saw_canonical_dynamic = false;
82
83 for output in &config.outputs {
84 let file_name = std::path::Path::new(&output.file_name);
85 if file_name.components().count() != 1 {
86 return Err(BundleSchemaError(format!(
87 "bundle output {} is not a single path component",
88 output.file_name
89 )));
90 }
91 match output.media_type.as_str() {
92 STOW_RLIB_MEDIA_TYPE => {
93 saw_canonical_rlib |= output.file_name == format!("{canonical_stem}.rlib");
94 }
95 STOW_RMETA_MEDIA_TYPE => {
96 saw_canonical_rmeta |= output.file_name == format!("{canonical_stem}.rmeta");
97 }
98 STOW_DYLIB_MEDIA_TYPE | STOW_PROC_MACRO_MEDIA_TYPE => {
99 saw_canonical_dynamic |=
100 output.file_name.starts_with(&format!("{canonical_stem}."));
101 }
102 other => {
103 return Err(BundleSchemaError(format!(
104 "bundle output {} has unsupported media type {}",
105 output.file_name, other
106 )));
107 }
108 }
109 }
110
111 if config
112 .outputs
113 .iter()
114 .any(|output| output.media_type == STOW_RLIB_MEDIA_TYPE)
115 && !saw_canonical_rlib
116 {
117 return Err(BundleSchemaError(format!(
118 "bundle is missing canonical rlib output for stable metadata {}",
119 config.c_metadata
120 )));
121 }
122 if config
123 .outputs
124 .iter()
125 .any(|output| output.media_type == STOW_RMETA_MEDIA_TYPE)
126 && !saw_canonical_rmeta
127 {
128 return Err(BundleSchemaError(format!(
129 "bundle is missing canonical rmeta output for stable metadata {}",
130 config.c_metadata
131 )));
132 }
133 if config.outputs.iter().any(|output| {
134 output.media_type == STOW_DYLIB_MEDIA_TYPE
135 || output.media_type == STOW_PROC_MACRO_MEDIA_TYPE
136 }) && !saw_canonical_dynamic
137 {
138 return Err(BundleSchemaError(format!(
139 "bundle is missing canonical dynamic output for stable metadata {}",
140 config.c_metadata
141 )));
142 }
143
144 Ok(())
145}
146
147#[cfg(test)]
148mod tests {
149 use std::io::Cursor;
150
151 use super::validate_bundle_schema;
152 use crate::artifact::{ArtifactKind, RustCrateType};
153 use crate::bundle::{
154 ArtifactBlobConfig, ArtifactBundleFile, ArtifactBundleManifest, STOW_BUNDLE_MANIFEST_PATH,
155 STOW_RLIB_MEDIA_TYPE, STOW_RMETA_MEDIA_TYPE,
156 };
157 use crate::identity::{
158 CMetadata, CrateName, DependencyCMetadataIdentity, DependencyCMetadataJson, FeaturesJson,
159 TargetTriple, WireRustcVersion,
160 };
161 use crate::platform::{PanicStrategy, Profile, StripLevel};
162 use tar::{Builder, Header};
163
164 fn profile() -> Profile {
165 Profile {
166 opt_level: "0".to_owned(),
167 debuginfo: 1,
168 debug_assertions: true,
169 overflow_checks: true,
170 panic: PanicStrategy::Unwind,
171 strip: StripLevel::None,
172 }
173 }
174
175 fn bundle_file(file_name: &str, media_type: &str) -> ArtifactBundleFile {
176 ArtifactBundleFile {
177 file_name: file_name.to_owned(),
178 media_type: media_type.to_owned(),
179 sha256: "deadbeef".to_owned(),
180 }
181 }
182
183 fn stable_config(outputs: Vec<ArtifactBundleFile>) -> ArtifactBlobConfig {
184 let dependency_identity = DependencyCMetadataIdentity {
185 crate_name: CrateName::parse("unicode_ident").unwrap(),
186 c_metadata: CMetadata::parse(
187 "0e63365407e7f07c2be3d7da23fc1e46fdf371b2b1e7030e54325461657e757f",
188 )
189 .unwrap(),
190 };
191 ArtifactBlobConfig {
192 compile_key: "df1c5df8d44a9ede068e852b56a99270d4d6b905ee849e7f4861e2c13699f43e"
193 .to_owned(),
194 crate_name: CrateName::parse("proc-macro2").unwrap(),
195 crate_version: "1.0.106".parse().unwrap(),
196 c_metadata: CMetadata::parse("df1c5df8d44a9ede").unwrap(),
197 extra_filename: "-df1c5df8d44a9ede".to_owned(),
198 target: TargetTriple::parse("aarch64-apple-darwin").unwrap(),
199 rustc_version: WireRustcVersion::parse("1.91.1").unwrap(),
200 features_json: FeaturesJson::canonicalize(vec![
201 "default".to_owned(),
202 "proc-macro".to_owned(),
203 ])
204 .unwrap(),
205 dependency_compile_keys_json: DependencyCMetadataJson::from_sorted(vec![
206 dependency_identity.clone(),
207 ])
208 .unwrap()
209 .raw(),
210 dependency_c_metadata_json: DependencyCMetadataJson::from_sorted(vec![
211 dependency_identity,
212 ])
213 .unwrap(),
214 profile: profile(),
215 emit: vec![
216 "dep-info".to_owned(),
217 "link".to_owned(),
218 "metadata".to_owned(),
219 ],
220 artifact_size: 1,
221 compile_millis: 0,
222 kind: ArtifactKind::Rlib,
223 crate_types: vec![RustCrateType::Lib],
224 outputs,
225 native: None,
226 native_archive: None,
227 }
228 }
229
230 fn bundle_bytes(config: ArtifactBlobConfig) -> Vec<u8> {
231 let manifest = ArtifactBundleManifest {
232 oci_reference: "ghcr.io/water-rs/stow-cache:proc-macro2.test".to_owned(),
233 oci_digest: "sha256:test".to_owned(),
234 config,
235 sigstore_signatures: Vec::new(),
236 };
237 let manifest_json = serde_json::to_vec(&manifest).unwrap();
238 let mut tar = Builder::new(Vec::new());
239 let mut header = Header::new_gnu();
240 header.set_size(manifest_json.len() as u64);
241 header.set_mode(0o644);
242 header.set_cksum();
243 tar.append_data(
244 &mut header,
245 STOW_BUNDLE_MANIFEST_PATH,
246 Cursor::new(manifest_json),
247 )
248 .unwrap();
249 tar.into_inner().unwrap()
250 }
251
252 #[test]
253 fn validate_bundle_schema_rejects_stable_bundle_without_canonical_output_names() {
254 let bytes = bundle_bytes(stable_config(vec![
255 bundle_file("libproc_macro2-68afcc2f66100859.rlib", STOW_RLIB_MEDIA_TYPE),
256 bundle_file(
257 "libproc_macro2-68afcc2f66100859.rmeta",
258 STOW_RMETA_MEDIA_TYPE,
259 ),
260 ]));
261
262 assert!(validate_bundle_schema(&bytes).is_err());
263 }
264
265 #[test]
266 fn validate_bundle_schema_accepts_stable_bundle_with_canonical_output_names() {
267 let bytes = bundle_bytes(stable_config(vec![
268 bundle_file("libproc_macro2-57f123ce754eb51b.rlib", STOW_RLIB_MEDIA_TYPE),
269 bundle_file("libproc_macro2-df1c5df8d44a9ede.rlib", STOW_RLIB_MEDIA_TYPE),
270 bundle_file(
271 "libproc_macro2-57f123ce754eb51b.rmeta",
272 STOW_RMETA_MEDIA_TYPE,
273 ),
274 bundle_file(
275 "libproc_macro2-df1c5df8d44a9ede.rmeta",
276 STOW_RMETA_MEDIA_TYPE,
277 ),
278 ]));
279
280 validate_bundle_schema(&bytes).unwrap();
281 }
282}