1use crate::{
5 entity::entity,
6 geometry::{FracVal, GridPoint, GridRect, PinSlot, ScreenRect, Waypoint},
7 hash::AssetHash,
8 id::{AreaId, BlockId, ImageId, PinId, RouteId, RouteLabelId, TextId},
9 values::{LabelSide, PinDir, Role},
10};
11use serde::{Deserialize, Serialize};
12use std::num::NonZeroU32;
13use std::sync::Arc;
14
15#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
18pub struct Icon {
19 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
20 pub asset: AssetHash,
21 #[serde(default, skip_serializing_if = "crate::entity::is_default")]
22 pub rect: ScreenRect,
23}
24
25entity! {
26 pub struct Label(update LabelUpdate, id ()) {
28 registers {
29 Name => name: String,
30 Side => side: LabelSide,
31 Offset => offset: FracVal,
32 Hidden => hidden: bool,
33 }
34 namespaces {}
35 constants {}
36 }
37}
38
39entity! {
40 pub struct Block(update BlockUpdate, id BlockId) {
41 registers {
42 Parent => parent: BlockId,
44 Rect => rect: GridRect,
45 Locked => locked: bool,
46 Role => role: Role,
47 Icon => icon: Icon,
48 }
49 namespaces {
50 Title => title: Label,
51 TypeLabel => type_label: Label,
52 }
53 constants {}
54 }
55}
56
57entity! {
58 pub struct Pin(update PinUpdate, id PinId) {
59 registers {
60 Owner => owner: BlockId,
61 Name => name: String,
62 TypeName => type_name: String,
63 Tag => tag: String,
64 TagHidden => tag_hidden: bool,
65 Rect => rect: GridRect,
69 Slot => slot: PinSlot,
70 Dir => dir: PinDir,
71 PortAccent => port_accent: Role,
75 FlipLR => flip_lr: bool,
79 }
80 namespaces {}
81 constants {}
82 }
83}
84
85entity! {
86 pub struct Route(update RouteUpdate, id RouteId) {
87 registers {
88 Owner => owner: BlockId,
89 Name => name: String,
90 Role => role: Role,
91 Waypoints => waypoints: Vec<Waypoint>,
92 }
93 namespaces {}
94 constants {
95 from: PinId,
98 to: PinId,
99 }
100 }
101}
102
103entity! {
104 pub struct RouteLabel(update RouteLabelUpdate, id RouteLabelId) {
105 registers {
106 Owner => owner: RouteId,
107 Pos => pos: FracVal,
109 }
110 namespaces {}
111 constants {}
112 }
113}
114
115entity! {
116 pub struct Text(update TextUpdate, id TextId) {
117 registers {
118 Owner => owner: BlockId,
119 Text => text: String,
120 Pos => pos: GridPoint,
121 Role => role: Role,
122 Width => width: Option<NonZeroU32>,
125 }
126 namespaces {}
127 constants {}
128 }
129}
130
131entity! {
132 pub struct Area(update AreaUpdate, id AreaId) {
133 registers {
134 Owner => owner: BlockId,
135 Rect => rect: GridRect,
136 Role => role: Role,
137 }
138 namespaces {
139 Title => title: Label,
140 }
141 constants {}
142 }
143}
144
145entity! {
146 pub struct Image(update ImageUpdate, id ImageId) {
148 registers {
149 Owner => owner: BlockId,
150 Asset => asset: AssetHash,
151 Rect => rect: ScreenRect,
152 }
153 namespaces {}
154 constants {}
155 }
156}
157
158#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
164#[serde(rename_all = "lowercase")]
165pub enum Asset {
166 Svg(#[serde(with = "svg_payload")] Arc<[u8]>),
167 Png(#[serde(with = "png_payload")] Arc<[u8]>),
168}
169
170pub const ASSET_LIMIT: usize = 4 * 1024 * 1024;
176
177impl Asset {
178 pub fn bytes(&self) -> &[u8] {
179 match self {
180 Asset::Svg(bytes) | Asset::Png(bytes) => bytes,
181 }
182 }
183
184 pub fn within_limit(&self) -> bool {
186 self.bytes().len() <= ASSET_LIMIT
187 }
188
189 pub fn hash(&self) -> AssetHash {
191 AssetHash::of(self.bytes())
192 }
193}
194
195mod svg_payload {
201 use serde::{Deserializer, Serializer, ser};
202 use std::sync::Arc;
203
204 pub fn serialize<S: Serializer>(bytes: &Arc<[u8]>, serializer: S) -> Result<S::Ok, S::Error> {
205 if serializer.is_human_readable() {
206 let text = std::str::from_utf8(bytes).map_err(ser::Error::custom)?;
207 serializer.serialize_str(text)
208 } else {
209 serializer.serialize_bytes(bytes)
210 }
211 }
212
213 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<[u8]>, D::Error> {
214 if deserializer.is_human_readable() {
215 deserializer.deserialize_any(super::payload::Verbatim)
216 } else {
217 deserializer.deserialize_byte_buf(super::payload::Verbatim)
218 }
219 }
220}
221
222mod png_payload {
226 use base64::{Engine as _, engine::general_purpose::STANDARD};
227 use serde::{Deserializer, Serializer};
228 use std::sync::Arc;
229
230 pub fn serialize<S: Serializer>(bytes: &Arc<[u8]>, serializer: S) -> Result<S::Ok, S::Error> {
231 if serializer.is_human_readable() {
232 serializer.serialize_str(&STANDARD.encode(bytes))
233 } else {
234 serializer.serialize_bytes(bytes)
235 }
236 }
237
238 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<[u8]>, D::Error> {
239 if deserializer.is_human_readable() {
240 deserializer.deserialize_any(super::payload::Base64)
241 } else {
242 deserializer.deserialize_byte_buf(super::payload::Base64)
243 }
244 }
245}
246
247mod payload {
253 use base64::{Engine as _, engine::general_purpose::STANDARD};
254 use serde::de;
255 use std::sync::Arc;
256
257 pub struct Verbatim;
259
260 pub struct Base64;
262
263 macro_rules! payload_visitor {
264 ($visitor:ident, $expecting:literal, |$text:ident| $decode:expr) => {
265 impl<'de> de::Visitor<'de> for $visitor {
266 type Value = Arc<[u8]>;
267
268 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 f.write_str($expecting)
270 }
271
272 fn visit_str<E: de::Error>(self, $text: &str) -> Result<Self::Value, E> {
273 $decode
274 }
275
276 fn visit_bytes<E: de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
277 Ok(Arc::from(bytes))
278 }
279
280 fn visit_byte_buf<E: de::Error>(self, bytes: Vec<u8>) -> Result<Self::Value, E> {
281 Ok(Arc::from(bytes))
282 }
283
284 fn visit_seq<A: de::SeqAccess<'de>>(
285 self,
286 mut seq: A,
287 ) -> Result<Self::Value, A::Error> {
288 let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or_default());
289 while let Some(byte) = seq.next_element::<u8>()? {
290 bytes.push(byte);
291 }
292 Ok(Arc::from(bytes))
293 }
294 }
295 };
296 }
297
298 payload_visitor!(
299 Verbatim,
300 "a byte string, source text, or a sequence of bytes",
301 |text| Ok(Arc::from(text.as_bytes()))
302 );
303 payload_visitor!(
304 Base64,
305 "a byte string, base64 text, or a sequence of bytes",
306 |text| STANDARD
307 .decode(text)
308 .map(Arc::from)
309 .map_err(de::Error::custom)
310 );
311}
312
313#[cfg(test)]
314mod tests {
315 use super::{ASSET_LIMIT, Asset};
316
317 fn svg() -> Asset {
318 Asset::Svg(b"<svg/>".as_slice().into())
319 }
320
321 #[test]
325 fn a_payload_round_trips_through_json_in_its_own_spelling() {
326 let text = serde_json::to_string(&svg()).expect("it serializes");
327 assert_eq!(text, r#"{"svg":"<svg/>"}"#, "verbatim source text");
328 assert_eq!(
329 serde_json::from_str::<Asset>(&text).expect("and parses back"),
330 svg(),
331 );
332
333 let png = Asset::Png(b"\x89PNG".as_slice().into());
334 let text = serde_json::to_string(&png).expect("it serializes");
335 assert_eq!(text, r#"{"png":"iVBORw=="}"#, "standard-alphabet base64");
336 assert_eq!(
337 serde_json::from_str::<Asset>(&text).expect("and parses back"),
338 png,
339 );
340 }
341
342 #[test]
345 fn a_payload_written_as_a_number_array_still_reads() {
346 assert_eq!(
347 serde_json::from_str::<Asset>(r#"{"svg":[60,115,118,103,47,62]}"#)
348 .expect("the number-array spelling parses"),
349 svg(),
350 );
351 assert_eq!(
352 serde_json::from_str::<Asset>(r#"{"png":[]}"#).expect("an empty one too"),
353 Asset::Png(b"".as_slice().into()),
354 );
355 }
356
357 #[test]
358 fn png_text_that_is_not_base64_is_refused_rather_than_taken_as_its_own_bytes() {
359 assert!(serde_json::from_str::<Asset>(r#"{"png":"not base64!!"}"#).is_err());
360 }
361
362 #[test]
367 fn a_binary_format_still_writes_a_byte_string() {
368 let mut bytes = Vec::new();
369 ciborium::into_writer(&Asset::Png(b"hi".as_slice().into()), &mut bytes)
370 .expect("it serializes");
371 assert_eq!(bytes, b"\xa1cpngBhi", "a map of one, then a 2-byte string");
372 assert_eq!(
373 ciborium::from_reader::<Asset, _>(bytes.as_slice()).expect("and parses back"),
374 Asset::Png(b"hi".as_slice().into()),
375 );
376 }
377
378 #[test]
383 fn the_text_form_of_a_png_is_a_third_larger_than_the_payload() {
384 let big = Asset::Png(vec![0u8; 3 * 1024].into());
385 let text = serde_json::to_string(&big).expect("it serializes");
386 assert!(
387 text.len() > 4 * 1024 && text.len() < 5 * 1024,
388 "{}",
389 text.len()
390 );
391 assert_eq!(ASSET_LIMIT, 4 * 1024 * 1024);
392 }
393}