1//! Snowbound's own files in a notebook folder, kept in `.snowbound`. OneNote 2010 makes a
2//! section group of every subfolder but one with the Windows hidden attribute, so the
3//! folder takes the attribute when made, and again whenever Snowbound writes to it; the dot
4//! name hides it on Samba's defaults, macOS and Linux as well. Each feature keeps its own
5//! names inside, and a notebook without the folder is whole.
6//!
7//! Tag art: `tags.json` maps a tag, by the name and symbol its definition stores, to a
8//! picture in `tags/` named for its content, `<SHA-256>.png` or `.svg`, which is never
9//! rewritten. A writer rereads the mapping, merges its own and replaces the file, then reads
10//! it back and merges again until its own holds. The merge keeps every tag either side
11//! mapped; where both mapped one tag, the later mapping wins, then the greater art name, so
12//! writers agree whatever order they read in.
13
14use crate::{Result, session::Storage};
15use serde::{Deserialize, Serialize};
16use std::io;
17
18const FOLDER: &str = ".snowbound";
19const MAPPING: &str = ".snowbound/tags.json";
20/// Live presence's room secret: whoever reads the notebook's files may see who else has it
21/// open. Live Share never serves it to guests, who meet in the share's own room.
22const ROOM: &str = ".snowbound/live.json";
23const ART: &str = ".snowbound/tags";
24/// The most bytes read of a mapping or a picture.
25pub const LIMIT: usize = 1 << 20;
26
27/// A tag drawn with art of Snowbound's.
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct TagMapping {
30 /// The tag's name and symbol, as its definition stores them.
31 pub name: String,
32 pub shape: u16,
33 /// Its picture in `tags/`.
34 pub art: String,
35 /// When it was mapped, as a FILETIME.
36 pub mapped: u64,
37}
38
39/// The name `tags/` keeps a picture under: its SHA-256 and its extension, `png` or `svg`.
40pub fn art_name(bytes: &[u8], extension: &str) -> String {
41 let digest = <sha2::Sha256 as sha2::Digest>::digest(bytes);
42 let hex: String = digest.iter().map(|byte| format!("{byte:02x}")).collect();
43 format!("{hex}.{extension}")
44}
45
46/// Whether `name` could be a picture's in `tags/`, and so names nothing else.
47fn art_named(name: &str) -> bool {
48 name.split_once('.').is_some_and(|(hash, extension)| {
49 hash.len() == 64
50 && hash
51 .bytes()
52 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
53 && matches!(extension, "png" | "svg")
54 })
55}
56
57/// Merges `mappings` into `into`, by the rule the module describes.
58pub fn merge(into: &mut Vec<TagMapping>, mappings: impl IntoIterator<Item = TagMapping>) {
59 for mapping in mappings {
60 match into
61 .iter_mut()
62 .find(|kept| kept.name == mapping.name && kept.shape == mapping.shape)
63 {
64 Some(kept) => {
65 if (mapping.mapped, &mapping.art) > (kept.mapped, &kept.art) {
66 *kept = mapping;
67 }
68 }
69 None => into.push(mapping),
70 }
71 }
72}
73
74/// The mappings `tags.json` holds: none without it, and none it holds unreadably.
75pub(crate) fn mappings(storage: &dyn Storage) -> Result<Vec<TagMapping>> {
76 let bytes = match storage.read_file(MAPPING, LIMIT) {
77 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
78 return Ok(Vec::new());
79 }
80 bytes => bytes?,
81 };
82 let values: Vec<serde_json::Value> = serde_json::from_slice(&bytes).unwrap_or_default();
83 let mut mappings = Vec::new();
84 merge(
85 &mut mappings,
86 values
87 .into_iter()
88 .filter_map(|value| serde_json::from_value::<TagMapping>(value).ok())
89 .filter(|mapping| art_named(&mapping.art)),
90 );
91 Ok(mappings)
92}
93
94/// Picture `art` from `tags/`, once its bytes match its name.
95pub(crate) fn art(storage: &dyn Storage, art: &str) -> Result<Vec<u8>> {
96 let extension = art.rsplit('.').next().unwrap_or_default();
97 if !art_named(art) {
98 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
99 }
100 let bytes = storage.read_file(&format!("{ART}/{art}"), LIMIT)?;
101 if art_name(&bytes, extension) != art {
102 return Err(io::Error::from(io::ErrorKind::InvalidData).into());
103 }
104 Ok(bytes)
105}
106
107/// Maps tag `name` with symbol `shape` to picture `bytes`, a PNG or SVG as `extension`
108/// says, now; returns the mappings as they then stand.
109pub(crate) fn map(
110 storage: &dyn Storage,
111 name: &str,
112 shape: u16,
113 bytes: &[u8],
114 extension: &str,
115) -> Result<Vec<TagMapping>> {
116 if !matches!(extension, "png" | "svg") || bytes.len() > LIMIT {
117 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
118 }
119 let mapping = TagMapping {
120 name: name.to_owned(),
121 shape,
122 art: art_name(bytes, extension),
123 mapped: crate::now(),
124 };
125 for folder in [FOLDER, ART] {
126 match storage.create_directory(folder) {
127 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::AlreadyExists => {}
128 created => created?,
129 }
130 }
131 storage.hide(FOLDER)?;
132 let picture = format!("{ART}/{}", mapping.art);
133 if !storage.exists(&picture) {
134 let written = temporary(&picture);
135 storage.create(&written, bytes)?;
136 // Another writer may have kept the same picture meanwhile.
137 if let Err(error) = storage.rename(&written, &picture) {
138 storage.delete(&written)?;
139 if !storage.exists(&picture) {
140 return Err(error);
141 }
142 }
143 }
144 for _ in 0..3 {
145 let mut merged = mappings(storage)?;
146 merge(&mut merged, [mapping.clone()]);
147 let written = temporary(MAPPING);
148 let json = serde_json::to_vec_pretty(&merged).map_err(io::Error::from)?;
149 storage.create(&written, &json)?;
150 storage.replace(&written, MAPPING)?;
151 let kept = mappings(storage)?;
152 let mut held = kept.clone();
153 merge(&mut held, [mapping.clone()]);
154 if held == kept {
155 return Ok(kept);
156 }
157 }
158 Err(io::Error::from(io::ErrorKind::ResourceBusy).into())
159}
160
161/// The secret of the notebook's presence room (`live::Room::Notebook`), made where it has none.
162/// The first writer's stays: a writer that finds one made meanwhile takes it.
163pub(crate) fn room(storage: &dyn Storage) -> Result<[u8; 16]> {
164 #[derive(Serialize, Deserialize)]
165 struct Room {
166 room: String,
167 }
168 let read = || -> Result<Option<[u8; 16]>> {
169 let bytes = match storage.read_file(ROOM, LIMIT) {
170 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
171 return Ok(None);
172 }
173 bytes => bytes?,
174 };
175 let room: Room = serde_json::from_slice(&bytes).map_err(io::Error::from)?;
176 let secret: Option<Vec<u8>> = (0..room.room.len())
177 .step_by(2)
178 .map(|at| u8::from_str_radix(room.room.get(at..at + 2)?, 16).ok())
179 .collect();
180 Ok(Some(
181 secret
182 .and_then(|secret| secret.try_into().ok())
183 .ok_or(io::Error::from(io::ErrorKind::InvalidData))?,
184 ))
185 };
186 if let Some(secret) = read()? {
187 return Ok(secret);
188 }
189 match storage.create_directory(FOLDER) {
190 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::AlreadyExists => {}
191 created => created?,
192 }
193 storage.hide(FOLDER)?;
194 let mut secret = [0; 16];
195 getrandom::fill(&mut secret).map_err(|_| io::Error::other("System random source failed"))?;
196 let room = Room {
197 room: secret.iter().map(|byte| format!("{byte:02x}")).collect(),
198 };
199 let written = temporary(ROOM);
200 storage.create(
201 &written,
202 &serde_json::to_vec(&room).map_err(io::Error::from)?,
203 )?;
204 if storage.rename(&written, ROOM).is_err() {
205 storage.delete(&written)?;
206 }
207 read()?.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound).into())
208}
209
210/// A name beside `path` no other writer picks.
211fn temporary(path: &str) -> String {
212 use std::hash::{BuildHasher, RandomState};
213 let unique = RandomState::new().hash_one((crate::fs::process_id(), crate::now()));
214 format!("{path}.{unique:016x}.tmp")
215}
216
217pub mod themes;
218
219#[cfg(test)]
220mod tests;