| 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 | |
| 14 | use crate::{Result, session::Storage}; |
| 15 | use serde::{Deserialize, Serialize}; |
| 16 | use std::io; |
| 17 | |
| 18 | const FOLDER: &str = ".snowbound"; |
| 19 | const 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. |
| 22 | const ROOM: &str = ".snowbound/live.json"; |
| 23 | const ART: &str = ".snowbound/tags"; |
| 24 | /// The most bytes read of a mapping or a picture. |
| 25 | pub const LIMIT: usize = 1 << 20; |
| 26 | |
| 27 | /// A tag drawn with art of Snowbound's. |
| 28 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 29 | pub 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`. |
| 40 | pub 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. |
| 47 | fn 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. |
| 58 | pub 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. |
| 75 | pub(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. |
| 95 | pub(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. |
| 109 | pub(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. |
| 163 | pub(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. |
| 211 | fn 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 | |
| 217 | pub mod themes; |
| 218 | |
| 219 | #[cfg(test)] |
| 220 | mod tests; |