blockworx_store/
assets.rs1use blockworx_doc::{block_model::Asset, hash::AssetHash};
18use serde::{Deserialize, Serialize};
19
20#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
24pub enum AssetFormat {
25 #[serde(rename = "svg")]
26 Svg,
27 #[serde(rename = "png")]
28 Png,
29}
30
31impl AssetFormat {
32 pub const ALL: [Self; 2] = [Self::Svg, Self::Png];
36
37 pub fn of(asset: &Asset) -> Self {
38 match asset {
39 Asset::Svg(_) => Self::Svg,
40 Asset::Png(_) => Self::Png,
41 }
42 }
43
44 pub fn extension(self) -> &'static str {
45 match self {
46 Self::Svg => "svg",
47 Self::Png => "png",
48 }
49 }
50
51 fn asset(self, bytes: Vec<u8>) -> Asset {
52 match self {
53 Self::Svg => Asset::Svg(bytes.into()),
54 Self::Png => Asset::Png(bytes.into()),
55 }
56 }
57
58 pub fn file_name(self, hash: AssetHash) -> String {
60 format!("{hash}.{}", self.extension())
61 }
62}
63
64pub trait AssetSource {
68 fn names(&self, hash: AssetHash) -> String;
72
73 fn find(&self, hash: AssetHash) -> std::io::Result<(AssetFormat, Vec<u8>)>;
80}
81
82pub trait AssetSink {
85 fn put(&mut self, hash: AssetHash, asset: &Asset) -> std::io::Result<()>;
90}
91
92#[derive(Debug)]
94pub enum Miss {
95 Unreadable(std::io::Error),
96 Corrupt {
100 found: AssetHash,
101 },
102}
103
104#[derive(Debug, thiserror::Error)]
106pub struct AssetFault {
107 pub hash: AssetHash,
108 pub at: String,
109 pub why: Miss,
110}
111
112impl std::fmt::Display for AssetFault {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 let path = &self.at;
115 match &self.why {
116 Miss::Unreadable(error) => write!(
117 f,
118 "the document places artwork the container no longer holds: {path} ({error})",
119 ),
120 Miss::Corrupt { found } => write!(
121 f,
122 "the document places artwork addressed as {named}, but {path} holds bytes that \
123 hash to {found} — the payload has been replaced",
124 named = self.hash,
125 ),
126 }
127 }
128}
129
130pub fn hydrate(source: &dyn AssetSource, hash: AssetHash) -> Result<Asset, AssetFault> {
135 let fault = |why| AssetFault {
136 hash,
137 at: source.names(hash),
138 why,
139 };
140 let (format, bytes) = source.find(hash).map_err(|e| fault(Miss::Unreadable(e)))?;
141 let found = AssetHash::of(&bytes);
142 if found != hash {
143 return Err(fault(Miss::Corrupt { found }));
144 }
145 Ok(format.asset(bytes))
146}
147
148#[derive(Default, Debug)]
151pub struct Held(blockworx_doc::hash::HashedMap<blockworx_doc::hash::AssetKind, Asset>);
152
153impl AssetSource for Held {
154 fn names(&self, hash: AssetHash) -> String {
155 format!("{hash} in this session")
156 }
157
158 fn find(&self, hash: AssetHash) -> std::io::Result<(AssetFormat, Vec<u8>)> {
159 self.0
160 .get(&hash)
161 .map(|asset| (AssetFormat::of(asset), asset.bytes().to_vec()))
162 .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
163 }
164}
165
166impl AssetSink for Held {
167 fn put(&mut self, hash: AssetHash, asset: &Asset) -> std::io::Result<()> {
168 self.0.entry(hash).or_insert_with(|| asset.clone());
169 Ok(())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use blockworx_doc::block_model::Asset;
177
178 #[test]
181 fn a_payload_is_filed_under_its_hash_and_its_format() {
182 let asset = Asset::Svg(b"<svg/>".as_slice().into());
183 assert_eq!(AssetFormat::of(&asset), AssetFormat::Svg);
184 assert_eq!(
185 AssetFormat::Svg.file_name(asset.hash()),
186 format!("{}.svg", asset.hash()),
187 );
188 assert_eq!(
189 serde_json::to_string(&AssetFormat::Svg).expect("it serializes"),
190 "\"svg\"",
191 );
192 assert_eq!(
193 AssetFormat::of(&Asset::Png(b"".as_slice().into())).extension(),
194 "png",
195 );
196 }
197
198 #[test]
201 fn a_payload_comes_back_by_hash_and_is_checked_against_it() {
202 let asset = Asset::Png(b"artwork".as_slice().into());
203 let mut store = Held::default();
204 store.put(asset.hash(), &asset).expect("it is held");
205
206 assert_eq!(hydrate(&store, asset.hash()).expect("it comes back"), asset);
207 assert!(matches!(
208 hydrate(&store, Asset::Svg(b"<svg/>".as_slice().into()).hash()),
209 Err(AssetFault {
210 why: Miss::Unreadable(_),
211 ..
212 }),
213 ));
214 }
215}