1use std::{collections::HashMap, io::Read};
2
3use flate2::read::GzDecoder;
4use regex::Regex;
5use tar::Archive;
6use thiserror::Error;
7use uuid::Uuid;
8
9use crate::{Package, PackageAssetBuilder};
10
11#[derive(Error, Debug)]
12pub enum PackageReadError {
13 #[error("Failed to read package: {0}")]
14 ReadError(#[from] std::io::Error),
15 #[error("Failed to decode gzip stream: {0}")]
16 GzipError(#[from] flate2::DecompressError),
17 #[error("Failed to parse UUID: {0}")]
18 UuidError(#[from] uuid::Error),
19 #[error("Invalid asset path: {0}")]
20 InvalidAssetPath(String),
21}
22
23pub fn read_package<R: Read>(r: R) -> Result<Package, PackageReadError> {
24 let mut package = Package::new();
25 let decoder = GzDecoder::new(r);
26 let mut archive = Archive::new(decoder);
27
28 let entries = archive.entries()?;
29
30 let regex = Regex::new(r"\.\/([0-9a-f]{32})\/(.*)").unwrap();
31
32 let mut found_assets: HashMap<Uuid, PackageAssetBuilder> = HashMap::new();
33 for entry in entries {
34 let mut entry = entry?;
35 let path = entry.path()?;
36 let path_str = path
37 .to_str()
38 .ok_or_else(|| PackageReadError::InvalidAssetPath(format!("{:?}", path)))?;
39
40 if let Some(captures) = regex.captures(path_str) {
41 let asset_guid = Uuid::parse_str(&captures[1])?;
42 let asset_path = &captures[2];
43
44 let builder = found_assets.entry(asset_guid).or_default();
45
46 match asset_path {
47 "pathname" => {
48 let mut pathname = String::new();
49 entry.read_to_string(&mut pathname)?;
50 builder.pathname = Some(pathname);
51 }
52 "preview.png" => {
53 let mut preview = Vec::new();
54 entry.read_to_end(&mut preview)?;
55 builder.preview = Some(preview);
56 }
57 "asset.meta" => {
58 let mut meta = Vec::new();
59 entry.read_to_end(&mut meta)?;
60 builder.meta = Some(meta);
61 }
62 "asset" => {
63 let mut data = Vec::new();
64 entry.read_to_end(&mut data)?;
65 builder.data = Some(data);
66 }
67 _ => {}
68 }
69 }
70 }
71
72 for (uuid, builder) in found_assets {
73 package.assets.insert(uuid, builder.build());
74 }
75
76 Ok(package)
77}
78
79#[cfg(test)]
80mod tests {
81 use std::{fs::File, path::PathBuf};
82
83 use uuid::uuid;
84
85 use super::*;
86
87 fn get_package_file(path: &str) -> File {
88 let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
89 root.push(format!("tests/{}.unitypackage", path));
90 File::open(root).unwrap()
91 }
92
93 #[test]
94 fn asset_simple_material() {
95 let package = read_package(get_package_file("simple-material")).unwrap();
96
97 assert_eq!(package.assets.len(), 1);
98 assert!(package
99 .assets
100 .contains_key(&uuid!("c24c6def6556015fb913fec2280e3315")));
101 }
102
103 #[test]
104 fn asset_simple_cube() {
105 let package = read_package(get_package_file("simple-cube")).unwrap();
106
107 assert_eq!(package.assets.len(), 2);
108 assert!(package
109 .assets
110 .contains_key(&uuid!("c24c6def6556015fb913fec2280e3315")));
111 assert!(package
112 .assets
113 .contains_key(&uuid!("8109a09196ba303c59774d4f4048f48c")));
114 }
115
116 #[test]
117 fn asset_invalid_uuid() {
118 let package = read_package(get_package_file("invalid-uuid")).unwrap();
119
120 assert_eq!(package.assets.len(), 0);
121 }
122
123 #[test]
124 fn empty_archive() {
125 let package = read_package(get_package_file("empty")).unwrap();
126
127 assert_eq!(package.assets.len(), 0);
128 }
129}