1use std::collections::{BTreeMap, BTreeSet};
4
5use phoxal_model::AssetId;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 ASSETS_DIR, BundleError, BundlePath, BundleRoot, DocumentError, Sha256Digest, open_bundle_file,
10 read_and_verify,
11};
12
13#[derive(phoxal_macros::DescribeWire, Clone, Debug, Default, Deserialize, Serialize)]
15#[serde(deny_unknown_fields)]
16pub struct AssetIndex {
17 pub(crate) entries: Vec<AssetRecord>,
18}
19
20impl AssetIndex {
21 pub fn from_bytes(assets: &BTreeMap<AssetId, Vec<u8>>) -> Result<Self, DocumentError> {
24 let entries = assets
25 .iter()
26 .map(|(id, bytes)| {
27 Ok(AssetRecord {
28 id: id.clone(),
29 path: BundlePath::new(format!("{ASSETS_DIR}/{}", id.as_str()))?,
30 size_bytes: bytes.len() as u64,
31 digest: Sha256Digest::of(bytes),
32 })
33 })
34 .collect::<Result<Vec<_>, DocumentError>>()?;
35 let index = Self { entries };
36 index.validate()?;
37 Ok(index)
38 }
39
40 #[must_use]
42 pub fn entries(&self) -> &[AssetRecord] {
43 &self.entries
44 }
45
46 pub(crate) fn validate(&self) -> Result<(), DocumentError> {
47 let mut ids = BTreeSet::new();
48 let mut paths = BTreeSet::new();
49 for entry in &self.entries {
50 if !entry.path.starts_with_directory(ASSETS_DIR) {
51 return Err(DocumentError::AssetOutsideAssets {
52 path: entry.path.clone(),
53 });
54 }
55 let expected = format!("{ASSETS_DIR}/{}", entry.id.as_str());
56 if entry.path.as_str() != expected {
57 return Err(DocumentError::AssetPathMismatch {
58 id: entry.id.clone(),
59 path: entry.path.clone(),
60 });
61 }
62 if !ids.insert(entry.id.clone()) {
63 return Err(DocumentError::DuplicateAssetId {
64 id: entry.id.clone(),
65 });
66 }
67 if !paths.insert(entry.path.clone()) {
68 return Err(DocumentError::DuplicateAssetPath {
69 path: entry.path.clone(),
70 });
71 }
72 }
73 Ok(())
74 }
75}
76
77#[derive(phoxal_macros::DescribeWire, Clone, Debug, Deserialize, PartialEq, Serialize)]
79#[serde(deny_unknown_fields)]
80pub struct AssetRecord {
81 pub(crate) id: AssetId,
82 pub(crate) path: BundlePath,
83 pub(crate) size_bytes: u64,
84 pub(crate) digest: Sha256Digest,
85}
86
87impl AssetRecord {
88 #[must_use]
89 pub const fn id(&self) -> &AssetId {
90 &self.id
91 }
92
93 #[must_use]
94 pub const fn path(&self) -> &BundlePath {
95 &self.path
96 }
97
98 #[must_use]
99 pub const fn size_bytes(&self) -> u64 {
100 self.size_bytes
101 }
102
103 #[must_use]
104 pub const fn digest(&self) -> Sha256Digest {
105 self.digest
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct ParticipantAssets {
112 root: BundleRoot,
113 entries: BTreeMap<AssetId, AssetRecord>,
114}
115
116impl ParticipantAssets {
117 pub(crate) fn new(root: BundleRoot, index: &AssetIndex) -> Self {
118 Self {
119 root,
120 entries: index
121 .entries
122 .iter()
123 .map(|entry| (entry.id.clone(), entry.clone()))
124 .collect(),
125 }
126 }
127
128 pub(crate) fn relocate(&mut self, path: std::path::PathBuf) {
129 self.root.relocate(path);
130 }
131
132 pub fn ids(&self) -> impl ExactSizeIterator<Item = &AssetId> {
134 self.entries.keys()
135 }
136
137 pub fn read(&self, id: &AssetId) -> Result<Vec<u8>, BundleError> {
140 let entry = self
141 .entries
142 .get(id)
143 .ok_or_else(|| BundleError::UndeclaredAsset { id: id.clone() })?;
144 let path = entry.path.filesystem_path(self.root.path());
145 let mut file = open_bundle_file(&self.root, &entry.path)?;
146 read_and_verify(&mut file, &path, entry.digest, Some(entry.size_bytes))
147 }
148}