1use std::collections::{HashMap, HashSet};
23use std::fs;
24use std::fs::File;
25use std::io::Read;
26
27use cargo_metadata::{CargoOpt, Metadata, Node, Package, PackageId, Resolve};
28use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
29use ring::digest::{Context, Digest, SHA256};
30use serde_json::Value;
31
32pub use declarations::ResourceDataDeclaration;
33pub use resource_encoding::ResourceEncoding;
34pub use specifications::ResourceSpecification;
35
36use crate::declarations::ResourceConsumerDeclaration;
37use crate::reporting::{DefaultReporter, ReportingTrait};
38use crate::specifications::{PackageDetails, ResourceConsumerSpecification, ResourceRequirement};
39
40mod resource_encoding;
41
42mod declarations;
43
44mod specifications;
45
46pub mod reporting;
47
48pub type ResourceName = String;
50
51pub type ResourceSha = String;
53
54pub fn collate_resources(source_manifest: &Utf8PathBuf) -> Result<Vec<ResourceSpecification>, String> {
62 let reporter = DefaultReporter {};
63 collate_resources_with_reporting(source_manifest, &reporter)
64}
65
66pub fn collate_resources_with_reporting(source_manifest: &Utf8PathBuf, reporter: &impl ReportingTrait) -> Result<Vec<ResourceSpecification>, String> {
75 collate_resources_with_crate_filter(source_manifest, reporter, |_| true)
76}
77
78pub fn collate_resources_with_crate_filter(
88 source_manifest: &Utf8PathBuf,
89 reporter: &impl ReportingTrait,
90 crate_filter: impl Fn(&Package) -> bool
91) -> Result<Vec<ResourceSpecification>, String> {
92 if !source_manifest.exists() {
93 Err(format!("Source manifest does not exist: {}", source_manifest))?
94 }
95 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
97 let metadata: Metadata = metadata_cmd
98 .manifest_path(&source_manifest)
99 .features(CargoOpt::AllFeatures)
100 .exec()
101 .unwrap();
102
103 let root_package = metadata.root_package()
105 .expect("Unexpected error finding the consuming crate - please run in a crate not a workspace.");
106
107 let packages_by_id = get_package_details(&metadata)?;
109
110 let child_packages = packages_by_id.iter()
112 .filter(|(_id, details)| details.is_dependency())
113 .map(|(_id, details)| &details.package)
114 .collect::<Vec<_>>();
115
116 let mut declared_resources: HashMap<String, ResourceSpecification> = HashMap::new();
118 for package in child_packages {
119 if crate_filter(package) {
120 get_package_resource_data(package, &mut declared_resources, reporter)?
121 }
122 }
123
124 let required_resources_spec = get_resource_requirement(&root_package, &declared_resources)?;
126
127 let resource_root = required_resources_spec.resource_root;
129 create_output_directory(&resource_root)?;
130
131 if required_resources_spec.required_resources.len() <= 0 {
132 reporter.report_no_resources_found();
133 return Ok(vec!());
134 }
135
136 let mut resolved_resources = vec!();
137 for res_req in required_resources_spec.required_resources {
138 let res_dec = declared_resources.get(&res_req.resource_name)
139 .ok_or_else(|| {
140 reporter.report_missing_resource(&res_req.resource_name);
141 format!("No resource found matching requirement {}", res_req.resource_name)
142 }
143 )?;
144 copy_resource(&res_req, &res_dec, &resource_root, reporter)?;
145 resolved_resources.push(res_dec);
146 }
147
148 let res = serde_json::to_string(&resolved_resources)
150 .expect("Unable to serialize the set of resolved resources");
151
152 let record_file_path = resource_root.join("resolved_resources.json");
153 fs::write(record_file_path, res).map_err(|e| format!("Failed writing record file:{:?}", e))?;
154
155 Ok(resolved_resources.into_iter().map(|r|r.clone()).collect())
156}
157
158fn get_package_details(metadata: &Metadata) -> Result<HashMap<PackageId, PackageDetails<'_>>, String> {
160 let mut packages_by_id: HashMap<PackageId, PackageDetails> = HashMap::new();
161 for ref package in metadata.packages.iter() {
163 packages_by_id.insert(
164 package.id.clone(),
165 PackageDetails::new(&package)
166 );
167 }
168 let root_package = metadata.root_package()
170 .ok_or("Unable to get root package")?;
171 let dep_graph_root: &Resolve = metadata.resolve.as_ref().ok_or("Missing dependency graph.")?;
173 let node_list = &dep_graph_root.nodes;
174 let node_map: HashMap<PackageId, &Node> = node_list.iter().map(|n| (n.id.clone(), n)).collect();
175 let mut processed_packages = HashSet::new();
178 let mut pending_nodes = vec!(node_map.get(&root_package.id).ok_or("Missing dependency node")?);
179 while let Some(node) = pending_nodes.pop() {
180 let details = packages_by_id.get_mut(&node.id).ok_or("Missing details.")?;
182 details.set_is_dependency();
183
184 processed_packages.insert(&node.id);
186
187 for pkg in &node.dependencies {
189 if !processed_packages.contains(pkg) {
190 pending_nodes.push(node_map.get(&pkg).ok_or("Missing details.")?);
191 }
192 }
193 }
194 Ok(packages_by_id)
195}
196
197fn get_package_resource_data(
199 package: &Package,
200 resources: &mut HashMap<String, ResourceSpecification>,
201 reporter: &impl ReportingTrait,
202) -> Result<(), String> {
203 let cargo_resource_metadata: &Value = &package.metadata["cargo_resources"];
205 if !cargo_resource_metadata.is_object() {
206 return Ok(()); }
208 let provides_metadata = &cargo_resource_metadata["provides"];
209 match provides_metadata {
210 Value::Array(resource_entries) => {
211 for resource_entry in resource_entries {
212 let declaration_result = serde_json::from_value::<ResourceDataDeclaration>(resource_entry.clone());
213 match declaration_result {
214 Ok(declaration) => {
215 let resolved_output_path = declaration
217 .output_path.
218 unwrap_or(declaration.crate_path.to_owned());
219 let resolved_name = declaration.resource_name.unwrap_or(
220 declaration.crate_path.file_name()
221 .expect("Illegal resource name").to_string().into()
222 );
223
224 if declaration.crate_path.is_absolute() {
226 Err(
227 format!(
228 "Crate {} declares an absolute resource path {}",
229 &package.name,
230 &declaration.crate_path
231 )
232 )?
233 }
234 if resolved_output_path.is_absolute() {
235 Err(
236 format!(
237 "Crate {} declares an absolute output path {}",
238 &package.name,
239 &resolved_output_path
240 )
241 )?
242 }
243
244 let full_source_path = package
245 .manifest_path.parent().expect("No manifest directory!")
246 .join(declaration.crate_path);
247 let data = ResourceSpecification {
248 declaring_crate_name: package.name.as_ref().to_owned(),
249 declaring_crate_version: package.version.to_owned(),
250 encoding: declaration.encoding.unwrap_or(ResourceEncoding::Txt),
251 full_crate_path: full_source_path,
252 output_path: resolved_output_path,
253 resource_name: resolved_name.to_owned(),
254 is_local: package.source.is_none()
255 };
256
257 if resources.contains_key(&resolved_name) {
259 reporter.report_duplicate_resource(
260 &resolved_name,
261 &resources.get(&resolved_name).unwrap().full_crate_path,
262 &data.full_crate_path
263 );
264 }
265 resources.insert(resolved_name.to_owned(), data);
266 }
267
268 Err(err) => {
269 reporter.report_malformed_resource_declaration(
270 &package.name,
271 &err
272 );
273 return Err(format!("Malformed resource declaration in {}: {}",
274 package.name,
275 err));
276 }
277 }
278 }
279 Ok(())
280 }
281 Value::Null => Ok(()),
282 _ => {
283 reporter.report_malformed_resources_section();
284 Err(
285 "unexpected type for [package.metadata.cargo_resources].provides in the json-metadata".to_owned()
286 )
287 }
288 }
289}
290
291fn get_resource_requirement(
293 package: &Package,
294 available_resources: &HashMap<String, ResourceSpecification>,
295) -> Result<ResourceConsumerSpecification, String> {
296 let cargo_resource_metadata: &Value = &package.metadata["cargo_resources"];
298
299 let consumer_declaration = match &cargo_resource_metadata {
301 Value::Null => ResourceConsumerDeclaration {
302 resource_root: None,
303 requires: None,
304 },
305 Value::Object(_) => {
306 serde_json::from_value(cargo_resource_metadata.clone())
307 .map_err(|e| format!("Unable to read consuming crates [package.metadata.cargo_resources]: {}", e.to_string()))?
308 }
309 _ => panic!("Misconfigured [package.metadata.cargo_resources] in consuming package.")
310 };
311
312 let resource_root = consumer_declaration.resource_root.unwrap_or(Utf8PathBuf::from("target/resources"));
313
314 let required_resources: Vec<ResourceRequirement> = match consumer_declaration.requires {
315 None => { available_resources.values().map(|res_spec| ResourceRequirement {
317 resource_name: res_spec.resource_name.to_owned(),
318 required_sha: None,
319 }).collect()
320 }
321 Some(declarations) => { declarations.into_iter().map(|dec| ResourceRequirement {
323 resource_name: dec.resource_name.to_owned(),
324 required_sha: dec.required_sha.to_owned(),
325 }).collect()
326 }
327 };
328
329 Ok(ResourceConsumerSpecification { resource_root, required_resources })
330}
331
332fn copy_resource(
334 res_req: &ResourceRequirement,
335 res_dec: &ResourceSpecification,
336 resource_root: &Utf8PathBuf,
337 reporter: &impl ReportingTrait,
338) -> Result<(), String> {
339 let output_resources_path = resource_root
340 .join(&res_dec.output_path);
341 verify_resource_is_in_root(&output_resources_path, &resource_root)?;
343
344 let output_directory = output_resources_path.parent().unwrap();
346 create_output_directory(output_directory)?;
347
348 let new_sha = hex::encode(get_file_sha(&res_dec.full_crate_path)?.as_ref());
350
351 match res_req.required_sha {
353 Some(ref req) => {
354 if *req != new_sha {
355 Err(
356 format!("Resource {} with sha {} does not match required sha {}.",
357 res_req.resource_name,
358 new_sha,
359 req
360 )
361 )?
362 }
363 }
364 _ => {}
365 }
366
367 let mut already_exists = false;
369 if output_resources_path.exists() {
370 let existing_sha = hex::encode(get_file_sha(&output_resources_path)?.as_ref());
371 if existing_sha == new_sha {
372 already_exists = true;
373 }
374 }
375
376 if !already_exists {
377 fs::copy(&res_dec.full_crate_path, &output_resources_path)
378 .map_err(|e|
379 format!("Unable to copy resource {} to {}: {}",
380 &res_dec.full_crate_path,
381 &output_resources_path,
382 e
383 )
384 )?;
385 }
386
387 reporter.report_resource_collection(already_exists, &new_sha, &output_resources_path);
388 Ok(())
389}
390
391fn get_file_sha(path: &Utf8PathBuf) -> Result<Digest, String> {
393 let mut sha = Context::new(&SHA256);
394 let mut file = File::open(path).map_err(|e| format!("Error opening {}, {}", path, e))?;
395 let mut buffer = [0; 4096]; loop {
398 let bytes_read = file.read(&mut buffer)
399 .map_err(|e| format!("Error calculating SHA256 {}", e))?;
400 if bytes_read == 0 {
401 break;
402 }
403 sha.update(&buffer[..bytes_read]);
404 }
405
406 Ok(sha.finish())
407}
408
409fn verify_resource_is_in_root(
411 resource_path: &Utf8PathBuf,
412 root_path: &Utf8PathBuf,
413) -> Result<(), String> {
414 let can_root_path = root_path.canonicalize_utf8()
415 .map_err(
416 |e| format!(
417 "Unable to canonicalize root path: {}: {}",
418 root_path,
419 e
420 )
421 )?;
422
423 if !resource_path.parent().is_some() {
425 return Ok(());
426 }
427 let mut walked_directory = Utf8PathBuf::new();
428 let target_components = resource_path.parent().unwrap().components();
429 for component in target_components {
430
431 walked_directory = walked_directory.join(component);
432 create_output_directory(&mut walked_directory)?;
433 }
434 let can_resource_path = resource_path.parent().unwrap().canonicalize_utf8()
435 .map_err(
436 |e| format!(
437 "Unable to canonicalize resource path: {}: {}",
438 resource_path,
439 e
440 )
441 )?;
442
443 if !can_resource_path.starts_with(&can_root_path) {
444 Err(
445 format!(
446 "Can't copy to {:?} as not in resource root {:?}",
447 can_resource_path,
448 can_root_path
449 )
450 )?
451 }
452 Ok(())
453}
454
455fn create_output_directory(output_dir: &Utf8Path) -> Result<(), String> {
457 if !output_dir.exists() {
458 fs::create_dir_all(&output_dir)
459 .map_err(|e|
460 format!("Unable to create output directory {}: {}", &output_dir, e)
461 )?
462 }
463 Ok(())
464}