| author | |
| committer | |
| log | e01c7cf1781d73fb57bc66e80bb1b3da9cafa9d2 |
| tree | 52da27f852271fb76252deef5cba466070c5e2c0 |
| parent | e9e724551a1f4aec871dbe9a3ce15eb97c1fe1a8 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
A host serves its notebook's storage verbs (notebook::live::share::Host) to
the peers in the share's room; a guest runs the replica, queue and merge it
runs on an SMB share against them (Notebook::open_hosted,
Section::resume_hosted, Background::hosted), so offline queueing, rebases and
conflict pages work as there and the host's files are written only by its own
storage. A guest meets the host in a short code's room (SPAKE2 on its words
and any password, numbered by the relay or by the host off it) and is
welcomed with the share's room and its random secret; a new share has a new
secret. Guests' commits are checked on the host's image first, paths outside
the notebook and presence's secret are refused, and large bodies travel in
answered 128 KiB chunks so a relay never queues much for a slow peer. A
notebook opens from its last listing while the host is away.
Presence meets in a room whose random secret is kept in .snowbound/live.json
(Notebook::presence_room) in place of the TOC's identity, which every copy of
a notebook holds for good. Live events now carry frames, meetings and
departures, Live::leave says Bye, and the joining end of a code gives up
after one wrong try, as the relay counts each one. onestore::Transaction gains
a byte form to carry a commit to another machine.
Assisted-by: claude-opus-5.515 files changed, 4053 insertions(+), 208 deletions(-)
crates/notebook/README.md+30-16| ... | @@ -533,24 +533,38 @@ Device and simulator builds link for iOS. Native acceptance uses disposable | ... | @@ -533,24 +533,38 @@ Device and simulator builds link for iOS. Native acceptance uses disposable |
| 533 | OneNote 2010 clients and Samba; it does not establish on-device execution or | 533 | OneNote 2010 clients and Samba; it does not establish on-device execution or |
| 534 | physical power-loss durability. | 534 | physical power-loss durability. |
| 535 | 535 | ||
| 536 | ## Live presence (feature `live`) | 536 | ## Live presence and Live Share (feature `live`) |
| 537 | 537 | ||
| 538 | `live::Live::start(hello, room, reach, relay, notify)` listens on a TCP port and, with a | 538 | `live::Live::start(hello, room, reach, relay, events)` listens on a TCP port and, with a |
| 539 | `Reach`, advertises `_snowbound._tcp` by mDNS on every network or on loopback alone, | 539 | `Reach`, advertises `_snowbound._tcp` by mDNS on every network or on loopback alone, |
| 540 | connecting to the peers in the same `Room` that it finds: a notebook's identity, or a code | 540 | connecting to the peers in the same `Room` that it finds: a notebook's or a share's random |
| 541 | typed on both (`Room::Code("4-violet-otter")`). With a `relay` (`wss://live.example.net`, | 541 | secret (`Room::Notebook`), or a code typed on both (`Room::join("4-violet-otter", password)`). |
| 542 | `crates/relay`) it also joins the room there and meets its peers through it; a code's words | 542 | With a `relay` (`wss://live.example.net`, `crates/relay`) it also joins the room there and |
| 543 | alone (`Room::Code("violet-otter")`) ask the relay for a number, and `code()` then has the | 543 | meets its peers through it; the end sharing a code (`Room::share`) has the relay number its |
| 544 | whole code. `connect(address)` meets a peer discovery did not find. Peers meet through | 544 | words, `code()` then has the whole code, and it burns a code after too many wrong tries. |
| 545 | SPAKE2 on the room's secret, then every frame is AES-256-GCM under the keys it agreed: its | 545 | `connect(address)` meets a peer discovery did not find. Peers meet through SPAKE2 on the |
| 546 | number, which is also its nonce, then a message kind and a CBOR map (`live::wire`). A frame | 546 | room's secret, then every frame is AES-256-GCM under the keys it agreed: its number, which is |
| 547 | lost, repeated, reordered or forged on the way fails where it lands; the connection is | 547 | also its nonce, then a message kind and a CBOR map (`live::wire`). A frame lost, repeated, |
| 548 | dropped as broken, nothing from it after the fault is applied, and the ends meet again from | 548 | reordered or forged on the way fails where it lands; the connection is dropped as broken, |
| 549 | scratch. A reader skips kinds and map keys it doesn't know, so later versions add both | 549 | nothing from it after the fault is applied, and the ends meet again from scratch. A reader |
| 550 | freely. `set_presence` says which section, page and caret this end has (text object and | 550 | skips kinds and map keys it doesn't know, so later versions add both freely. |
| 551 | UTF-16 offset, as ops address text); a connection sends only the newest. `peers()` lists | 551 | `set_presence` says which section, page and caret this end has (text object and UTF-16 |
| 552 | each connected peer's `Hello` (name, picture) and presence, and `notify` runs whenever that | 552 | offset, as ops address text); a connection sends only the newest. `peers()` lists each |
| 553 | changes. Dropping the `Live` leaves. | 553 | connected peer's `Hello` (name, picture) and presence; `events` hears when they change, who |
| 554 | was met and left, and every other frame with the `Line` to answer on. `leave(reason)` says | ||
| 555 | `Bye` first; dropping the `Live` leaves at once. `Notebook::presence_room()` is the secret of | ||
| 556 | a notebook's presence room, kept in `.snowbound/live.json` and made where it has none. | ||
| 557 | |||
| 558 | `live::share` is Live Share. `Host::start(storage, hello, sharing, name, reach, relay, events)` | ||
| 559 | serves `Notebook::into_storage()` to the peers in the share's room and welcomes whoever knows | ||
| 560 | `Sharing::code` (and its password) from the code's room; `code()` replaces a burned code with | ||
| 561 | new words, `guests()` lists who is connected, `touched(paths)` passes the host's own changes | ||
| 562 | on, and `stop()` lets every guest go. `join(hello, code, password, reach, relay)` returns the | ||
| 563 | `Welcome` (the share and its secret), or a `Refusal` saying why not: a wrong code, no one | ||
| 564 | sharing it, an expired code, too many wrong tries, or no relay. `Guest::start` joins the share; | ||
| 565 | `Notebook::open_hosted(guest, cache)`, `Background::hosted(guest, notify)` and | ||
| 566 | `Section::resume_hosted(path, replica, guest, notify)` then work as on a share, with | ||
| 567 | `Guest::host()` and `stopped()` for whether the host is there and still sharing. | ||
| 554 | 568 | ||
| 555 | ## Queue measurement | 569 | ## Queue measurement |
| 556 | 570 |
crates/notebook/src/background.rs+42-2| ... | @@ -62,10 +62,15 @@ pub(crate) struct Shared { | ... | @@ -62,10 +62,15 @@ pub(crate) struct Shared { |
| 62 | watched: Mutex<Watched>, | 62 | watched: Mutex<Watched>, |
| 63 | /// Asked for by `discard`, once the thread stops. | 63 | /// Asked for by `discard`, once the thread stops. |
| 64 | discard: AtomicBool, | 64 | discard: AtomicBool, |
| 65 | /// Hears every report of changed paths (`on_touched`). | ||
| 66 | listener: Mutex<Option<Listener>>, | ||
| 65 | } | 67 | } |
| 66 | 68 | ||
| 69 | /// Hears the catalog paths a report names. | ||
| 70 | pub type Listener = Box<dyn Fn(&[String]) + Send + Sync>; | ||
| 71 | |||
| 67 | /// A connection's report of its notebook folder's changes, from a watch armed on connecting. | 72 | /// A connection's report of its notebook folder's changes, from a watch armed on connecting. |
| 68 | #[cfg_attr(not(feature = "smb"), allow(dead_code))] | 73 | #[cfg_attr(not(any(feature = "smb", feature = "live")), allow(dead_code))] |
| 69 | pub(crate) struct Reports { | 74 | pub(crate) struct Reports { |
| 70 | shared: Weak<Shared>, | 75 | shared: Weak<Shared>, |
| 71 | connection: u64, | 76 | connection: u64, |
| ... | @@ -135,6 +140,7 @@ impl Background { | ... | @@ -135,6 +140,7 @@ impl Background { |
| 135 | signal, | 140 | signal, |
| 136 | watched: Mutex::default(), | 141 | watched: Mutex::default(), |
| 137 | discard: AtomicBool::new(false), | 142 | discard: AtomicBool::new(false), |
| 143 | listener: Mutex::default(), | ||
| 138 | }); | 144 | }); |
| 139 | let weak = Arc::downgrade(&shared); | 145 | let weak = Arc::downgrade(&shared); |
| 140 | let owner = Arc::clone(&shared); | 146 | let owner = Arc::clone(&shared); |
| ... | @@ -342,6 +348,35 @@ impl Background { | ... | @@ -342,6 +348,35 @@ impl Background { |
| 342 | ) | 348 | ) |
| 343 | } | 349 | } |
| 344 | 350 | ||
| 351 | /// Keeps the sections of a notebook a Live Share host serves in sync while they are not | ||
| 352 | /// open, with an offline copy of each; the host reports what changed. Paths are catalog | ||
| 353 | /// paths. | ||
| 354 | #[cfg(feature = "live")] | ||
| 355 | pub fn hosted( | ||
| 356 | guest: Arc<crate::live::share::Guest>, | ||
| 357 | notify: impl Fn() + Send + 'static, | ||
| 358 | ) -> Result<Self> { | ||
| 359 | Self::start( | ||
| 360 | true, | ||
| 361 | move |reports| { | ||
| 362 | guest.watch(reports)?; | ||
| 363 | let (bound, listing) = (Arc::clone(&guest), Arc::clone(&guest)); | ||
| 364 | let bind = move |path: &str| crate::live::share::HostedRemote::new(&bound, path); | ||
| 365 | let list = move |folder: &str| listing.entries(folder); | ||
| 366 | Ok(((bind, list), true)) | ||
| 367 | }, | ||
| 368 | notify, | ||
| 369 | ) | ||
| 370 | } | ||
| 371 | |||
| 372 | /// Has `listener` hear every report of changed paths from now on, as a Live Share host | ||
| 373 | /// passes them to its guests; `None` stops. | ||
| 374 | pub fn on_touched(&self, listener: Option<Listener>) { | ||
| 375 | if let Ok(mut held) = self.0.listener.lock() { | ||
| 376 | *held = listener; | ||
| 377 | } | ||
| 378 | } | ||
| 379 | |||
| 345 | /// Watches these sections (`Notebook::replicas`); a section not watched before is first | 380 | /// Watches these sections (`Notebook::replicas`); a section not watched before is first |
| 346 | /// checked soon after, the next one a little later. | 381 | /// checked soon after, the next one a little later. |
| 347 | pub fn watch(&self, sections: Vec<Known>) { | 382 | pub fn watch(&self, sections: Vec<Known>) { |
| ... | @@ -484,6 +519,11 @@ impl Shared { | ... | @@ -484,6 +519,11 @@ impl Shared { |
| 484 | watched.touched(paths, Instant::now()); | 519 | watched.touched(paths, Instant::now()); |
| 485 | } | 520 | } |
| 486 | self.signal.wake(); | 521 | self.signal.wake(); |
| 522 | if let Ok(listener) = self.listener.lock() | ||
| 523 | && let Some(listener) = &*listener | ||
| 524 | { | ||
| 525 | listener(paths); | ||
| 526 | } | ||
| 487 | } | 527 | } |
| 488 | 528 | ||
| 489 | /// The session holding the section at `path` let it go: it is checked now. | 529 | /// The session holding the section at `path` let it go: it is checked now. |
| ... | @@ -499,7 +539,7 @@ impl Shared { | ... | @@ -499,7 +539,7 @@ impl Shared { |
| 499 | } | 539 | } |
| 500 | } | 540 | } |
| 501 | 541 | ||
| 502 | #[cfg_attr(not(feature = "smb"), allow(dead_code))] | 542 | #[cfg_attr(not(any(feature = "smb", feature = "live")), allow(dead_code))] |
| 503 | impl Reports { | 543 | impl Reports { |
| 504 | /// The sections at or below these paths changed. | 544 | /// The sections at or below these paths changed. |
| 505 | pub(crate) fn touched(&self, paths: &[String]) { | 545 | pub(crate) fn touched(&self, paths: &[String]) { |
crates/notebook/src/live.rs+329-137| ... | @@ -1,16 +1,19 @@ | ... | @@ -1,16 +1,19 @@ |
| 1 | //! Live presence: who else has the notebook open, the page they are on and their caret, | 1 | //! Live presence and Live Share: who else has the notebook open, the page they are on and |
| 2 | //! straight from one Snowbound to another, or through a relay (`crates/relay`) where they | 2 | //! their caret, straight from one Snowbound to another, or through a relay (`crates/relay`) |
| 3 | //! aren't on one network. Peers find each other with mDNS (`_snowbound._tcp`) or in the | 3 | //! where they aren't on one network; and a notebook one machine holds opened on another |
| 4 | //! relay's room, and meet through a secret both hold, a notebook's identity or a code typed | 4 | //! (`share`). Peers find each other with mDNS (`_snowbound._tcp`) or in the relay's room, and |
| 5 | //! on both, which SPAKE2 turns into the keys every frame after the opening is sealed with. | 5 | //! meet through a secret both hold, a room's or a code typed on both, which SPAKE2 turns into |
| 6 | //! Each connection has a thread reading and one writing. A connection whose frames arrive | 6 | //! the keys every frame after the opening is sealed with. Each connection has a thread reading |
| 7 | //! out of order is dropped and met again from scratch. | 7 | //! and one writing. A connection whose frames arrive out of order is dropped and met again |
| 8 | //! from scratch. | ||
| 8 | 9 | ||
| 9 | mod relay; | 10 | mod relay; |
| 11 | pub mod share; | ||
| 10 | pub mod wire; | 12 | pub mod wire; |
| 11 | pub use wire::{Caret, Guid, Hello, Presence, Spot}; | 13 | pub use wire::{Caret, Guid, Hello, Presence, Spot}; |
| 12 | 14 | ||
| 13 | use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent, ServiceInfo}; | 15 | use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent, ServiceInfo}; |
| 16 | use minicbor::Encode; | ||
| 14 | use sha2::{Digest, Sha256}; | 17 | use sha2::{Digest, Sha256}; |
| 15 | use std::{ | 18 | use std::{ |
| 16 | collections::BTreeMap, | 19 | collections::BTreeMap, |
| ... | @@ -22,7 +25,7 @@ use std::{ | ... | @@ -22,7 +25,7 @@ use std::{ |
| 22 | mpsc, | 25 | mpsc, |
| 23 | }, | 26 | }, |
| 24 | thread, | 27 | thread, |
| 25 | time::Duration, | 28 | time::{Duration, Instant}, |
| 26 | }; | 29 | }; |
| 27 | use wire::{Sealer, Side, kind}; | 30 | use wire::{Sealer, Side, kind}; |
| 28 | 31 | ||
| ... | @@ -33,37 +36,73 @@ const GONE: Duration = Duration::from_secs(45); | ... | @@ -33,37 +36,73 @@ const GONE: Duration = Duration::from_secs(45); |
| 33 | const OPENING: Duration = Duration::from_secs(5); | 36 | const OPENING: Duration = Duration::from_secs(5); |
| 34 | /// The longest wait before meeting again. | 37 | /// The longest wait before meeting again. |
| 35 | const PATIENCE: Duration = Duration::from_secs(30); | 38 | const PATIENCE: Duration = Duration::from_secs(30); |
| 39 | /// Wrong tries of a code met off any relay before it admits no one new, as a relay burns one. | ||
| 40 | const TRIES: u32 = 5; | ||
| 36 | 41 | ||
| 37 | /// The secret peers meet through. | 42 | /// The secret peers meet through. |
| 38 | #[derive(Clone, Debug, PartialEq, Eq)] | 43 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 39 | pub enum Room { | 44 | pub enum Room { |
| 40 | /// Everyone with the notebook: its table of contents' file identity, which only its files | 45 | /// A notebook's room, or a share's: a random secret only its members hold. |
| 41 | /// hold. | ||
| 42 | Notebook([u8; 16]), | 46 | Notebook([u8; 16]), |
| 43 | /// A code typed on both: `7-violet-otter`, whose number names it on the network and whose | 47 | /// A code typed on both ends, `412-violet-otter`, with a password where one is set: its |
| 44 | /// words only the two people know. Its words alone (`violet-otter`) ask the relay for a | 48 | /// number names it on the network, and its words and password only the two people know. |
| 45 | /// free number, and `Live::code` then has the whole code. | 49 | /// Its `owner`, the end sharing it, takes the number from a relay (the one the code has, |
| 46 | Code(String), | 50 | /// coming back; any free one for words alone) or picks one where it has no relay, and |
| 51 | /// judges every try, burning the code after too many wrong ones. | ||
| 52 | Code { | ||
| 53 | code: String, | ||
| 54 | password: String, | ||
| 55 | owner: bool, | ||
| 56 | }, | ||
| 47 | } | 57 | } |
| 48 | 58 | ||
| 49 | impl Room { | 59 | impl Room { |
| 50 | /// What names the room in the clear: a hash of a notebook's identity, a code's number; | 60 | /// The room a code typed on this end leads to. |
| 51 | /// none for a code the relay hasn't numbered. | 61 | pub fn join(code: &str, password: &str) -> Self { |
| 62 | Self::Code { | ||
| 63 | code: code.trim().to_lowercase(), | ||
| 64 | password: password.to_owned(), | ||
| 65 | owner: false, | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// The room of a code this end shares: its words, or a whole code to keep its number. | ||
| 70 | pub fn share(code: &str, password: &str) -> Self { | ||
| 71 | Self::Code { | ||
| 72 | code: code.trim().to_lowercase(), | ||
| 73 | password: password.to_owned(), | ||
| 74 | owner: true, | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | /// What names the room in the clear: a hash of a room's secret, a code's number; none for | ||
| 79 | /// a code without one yet. | ||
| 52 | fn tag(&self) -> Option<String> { | 80 | fn tag(&self) -> Option<String> { |
| 53 | match self { | 81 | match self { |
| 54 | Room::Notebook(id) => Some(hex(&Sha256::digest( | 82 | Room::Notebook(id) => Some(hex(&Sha256::digest( |
| 55 | [&b"Snowbound room "[..], id].concat(), | 83 | [&b"Snowbound room "[..], id].concat(), |
| 56 | )[..8])), | 84 | )[..8])), |
| 57 | Room::Code(code) => code_parts(code).0.map(|number| format!("code-{number}")), | 85 | Room::Code { code, .. } => code_parts(code).0.map(|number| format!("code-{number}")), |
| 58 | } | 86 | } |
| 59 | } | 87 | } |
| 60 | 88 | ||
| 61 | fn secret(&self) -> Vec<u8> { | 89 | fn secret(&self) -> Vec<u8> { |
| 62 | match self { | 90 | match self { |
| 63 | Room::Notebook(id) => id.to_vec(), | 91 | Room::Notebook(id) => id.to_vec(), |
| 64 | Room::Code(code) => code_parts(code).1.to_lowercase().into_bytes(), | 92 | Room::Code { code, password, .. } => { |
| 93 | let mut secret = code_parts(code).1.to_lowercase().into_bytes(); | ||
| 94 | if !password.is_empty() { | ||
| 95 | secret.push(b'\n'); | ||
| 96 | secret.extend_from_slice(password.as_bytes()); | ||
| 97 | } | ||
| 98 | secret | ||
| 99 | } | ||
| 65 | } | 100 | } |
| 66 | } | 101 | } |
| 102 | |||
| 103 | fn owner(&self) -> bool { | ||
| 104 | matches!(self, Room::Code { owner: true, .. }) | ||
| 105 | } | ||
| 67 | } | 106 | } |
| 68 | 107 | ||
| 69 | /// A code's number, if it has one, and its words. | 108 | /// A code's number, if it has one, and its words. |
| ... | @@ -91,19 +130,49 @@ pub struct Peer { | ... | @@ -91,19 +130,49 @@ pub struct Peer { |
| 91 | pub presence: Option<Presence>, | 130 | pub presence: Option<Presence>, |
| 92 | } | 131 | } |
| 93 | 132 | ||
| 133 | /// What happened, as `Live::start`'s `events` hears it on a network thread. | ||
| 134 | pub enum Event<'a> { | ||
| 135 | /// The peers, their presence, the code or the relay's answer changed. | ||
| 136 | Changed, | ||
| 137 | /// A peer was met, with the line to it. | ||
| 138 | Met(&'a Arc<Hello>, &'a Line), | ||
| 139 | /// A peer's connection ended. | ||
| 140 | Left(&'a Arc<Hello>), | ||
| 141 | /// A frame of a kind presence doesn't read itself, with the line to answer on. | ||
| 142 | Frame { | ||
| 143 | from: &'a Arc<Hello>, | ||
| 144 | kind: u16, | ||
| 145 | body: &'a [u8], | ||
| 146 | line: &'a Line, | ||
| 147 | }, | ||
| 148 | } | ||
| 149 | |||
| 150 | /// How the relay last answered. | ||
| 151 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
| 152 | pub enum Relayed { | ||
| 153 | /// Not asked yet, or no relay. | ||
| 154 | Unknown, | ||
| 155 | /// In the room. | ||
| 156 | Joined, | ||
| 157 | /// Answered with this HTTP status, and how long it asked to wait. | ||
| 158 | Refused(u16, Option<Duration>), | ||
| 159 | /// Not reached. | ||
| 160 | Unreachable, | ||
| 161 | } | ||
| 162 | |||
| 94 | /// Presence on the network while it lives; dropping it leaves. | 163 | /// Presence on the network while it lives; dropping it leaves. |
| 95 | pub struct Live { | 164 | pub struct Live { |
| 96 | shared: Arc<Shared>, | 165 | shared: Arc<Shared>, |
| 97 | address: SocketAddr, | 166 | address: SocketAddr, |
| 98 | daemon: Option<ServiceDaemon>, | ||
| 99 | } | 167 | } |
| 100 | 168 | ||
| 101 | struct Shared { | 169 | struct Shared { |
| 102 | me: Hello, | 170 | me: Hello, |
| 103 | room: Room, | 171 | room: Room, |
| 104 | secret: Vec<u8>, | 172 | secret: Vec<u8>, |
| 173 | reach: Option<Reach>, | ||
| 105 | state: Mutex<State>, | 174 | state: Mutex<State>, |
| 106 | notify: Box<dyn Fn() + Send + Sync>, | 175 | events: Box<dyn Fn(Event) + Send + Sync>, |
| 107 | stopped: AtomicBool, | 176 | stopped: AtomicBool, |
| 108 | connections: AtomicU64, | 177 | connections: AtomicU64, |
| 109 | } | 178 | } |
| ... | @@ -118,15 +187,43 @@ struct State { | ... | @@ -118,15 +187,43 @@ struct State { |
| 118 | code: Option<String>, | 187 | code: Option<String>, |
| 119 | /// The relay connection open now, to hang up on leaving. | 188 | /// The relay connection open now, to hang up on leaving. |
| 120 | relay: Option<Arc<relay::Socket>>, | 189 | relay: Option<Arc<relay::Socket>>, |
| 190 | relayed: Option<Relayed>, | ||
| 191 | /// Meetings that failed on the secret: wrong codes tried here, or this end's. | ||
| 192 | failed: u32, | ||
| 193 | /// The code admits no one new. | ||
| 194 | burned: bool, | ||
| 195 | daemon: Option<ServiceDaemon>, | ||
| 121 | } | 196 | } |
| 122 | 197 | ||
| 123 | struct Link { | 198 | struct Link { |
| 124 | connection: u64, | 199 | connection: u64, |
| 125 | peer: Peer, | 200 | peer: Peer, |
| 126 | wake: mpsc::Sender<()>, | 201 | line: Line, |
| 127 | pipe: Arc<dyn Pipe>, | 202 | pipe: Arc<dyn Pipe>, |
| 128 | } | 203 | } |
| 129 | 204 | ||
| 205 | /// What a connection's writer sends next. | ||
| 206 | enum Out { | ||
| 207 | /// The newest presence. | ||
| 208 | Presence, | ||
| 209 | Frame(u16, Vec<u8>), | ||
| 210 | /// A `Bye`, after which it hangs up. | ||
| 211 | Bye(Vec<u8>), | ||
| 212 | } | ||
| 213 | |||
| 214 | /// The way to one connected peer: frames sent on it go after those sent before. | ||
| 215 | #[derive(Clone)] | ||
| 216 | pub struct Line(mpsc::Sender<Out>); | ||
| 217 | |||
| 218 | impl Line { | ||
| 219 | pub fn send(&self, kind: u16, body: &impl Encode<()>) -> io::Result<()> { | ||
| 220 | let body = minicbor::to_vec(body).map_err(io::Error::other)?; | ||
| 221 | self.0 | ||
| 222 | .send(Out::Frame(kind, body)) | ||
| 223 | .map_err(|_| io::ErrorKind::NotConnected.into()) | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 130 | /// A stream to one peer, read by one thread and written by another: a TCP connection, or | 227 | /// A stream to one peer, read by one thread and written by another: a TCP connection, or |
| 131 | /// one carried through a relay. | 228 | /// one carried through a relay. |
| 132 | trait Pipe: Send + Sync { | 229 | trait Pipe: Send + Sync { |
| ... | @@ -185,22 +282,14 @@ impl Write for &dyn Pipe { | ... | @@ -185,22 +282,14 @@ impl Write for &dyn Pipe { |
| 185 | impl Live { | 282 | impl Live { |
| 186 | /// Starts listening as `me` in `room`, advertised and looked for where `reach` says, or | 283 | /// Starts listening as `me` in `room`, advertised and looked for where `reach` says, or |
| 187 | /// not at all with `None`, leaving peers to `connect`, and in the room at `relay` | 284 | /// not at all with `None`, leaving peers to `connect`, and in the room at `relay` |
| 188 | /// (`wss://live.example.net`) where given. `notify` runs on a network thread whenever | 285 | /// (`wss://live.example.net`) where given. `events` runs on a network thread. |
| 189 | /// `peers` or `code` changes. | ||
| 190 | pub fn start( | 286 | pub fn start( |
| 191 | me: Hello, | 287 | me: Hello, |
| 192 | room: &Room, | 288 | room: &Room, |
| 193 | reach: Option<Reach>, | 289 | reach: Option<Reach>, |
| 194 | relay: Option<&str>, | 290 | relay: Option<&str>, |
| 195 | notify: impl Fn() + Send + Sync + 'static, | 291 | events: impl Fn(Event) + Send + Sync + 'static, |
| 196 | ) -> io::Result<Live> { | 292 | ) -> io::Result<Live> { |
| 197 | let tag = room.tag(); | ||
| 198 | if tag.is_none() && relay.is_none() { | ||
| 199 | return Err(io::Error::new( | ||
| 200 | io::ErrorKind::InvalidInput, | ||
| 201 | "Only a relay can number a code", | ||
| 202 | )); | ||
| 203 | } | ||
| 204 | let host = match reach { | 293 | let host = match reach { |
| 205 | Some(Reach::Network) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), | 294 | Some(Reach::Network) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), |
| 206 | Some(Reach::Loopback) | None => IpAddr::V4(Ipv4Addr::LOCALHOST), | 295 | Some(Reach::Loopback) | None => IpAddr::V4(Ipv4Addr::LOCALHOST), |
| ... | @@ -211,26 +300,36 @@ impl Live { | ... | @@ -211,26 +300,36 @@ impl Live { |
| 211 | address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); | 300 | address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); |
| 212 | } | 301 | } |
| 213 | let code = match room { | 302 | let code = match room { |
| 214 | Room::Code(code) if tag.is_some() => Some(code.trim().to_owned()), | 303 | Room::Code { code, .. } if room.tag().is_some() => Some(code.clone()), |
| 304 | // Off any relay, the end sharing words alone numbers them itself. | ||
| 305 | Room::Code { code, .. } if relay.is_none() => { | ||
| 306 | let mut number = [0; 2]; | ||
| 307 | getrandom::fill(&mut number) | ||
| 308 | .map_err(|_| io::Error::other("System random source failed"))?; | ||
| 309 | Some(format!( | ||
| 310 | "{}-{code}", | ||
| 311 | 1000 + u16::from_le_bytes(number) % 9000 | ||
| 312 | )) | ||
| 313 | } | ||
| 215 | _ => None, | 314 | _ => None, |
| 216 | }; | 315 | }; |
| 217 | let shared = Arc::new(Shared { | 316 | let shared = Arc::new(Shared { |
| 218 | me, | 317 | me, |
| 219 | room: room.clone(), | 318 | room: room.clone(), |
| 220 | secret: room.secret(), | 319 | secret: room.secret(), |
| 320 | reach, | ||
| 221 | state: Mutex::new(State { | 321 | state: Mutex::new(State { |
| 222 | code, | 322 | code, |
| 223 | ..State::default() | 323 | ..State::default() |
| 224 | }), | 324 | }), |
| 225 | notify: Box::new(notify), | 325 | events: Box::new(events), |
| 226 | stopped: AtomicBool::new(false), | 326 | stopped: AtomicBool::new(false), |
| 227 | connections: AtomicU64::new(0), | 327 | connections: AtomicU64::new(0), |
| 228 | }); | 328 | }); |
| 229 | if let Some(relay) = relay { | 329 | if let Some(relay) = relay { |
| 230 | relay::join(&shared, relay)?; | 330 | relay::join(&shared, relay, address.port())?; |
| 231 | } | 331 | } |
| 232 | let accepting = Arc::clone(&shared); | 332 | let accepting = Arc::clone(&shared); |
| 233 | let accepted = tag.clone(); | ||
| 234 | thread::Builder::new() | 333 | thread::Builder::new() |
| 235 | .name("live accept".into()) | 334 | .name("live accept".into()) |
| 236 | .spawn(move || { | 335 | .spawn(move || { |
| ... | @@ -238,23 +337,14 @@ impl Live { | ... | @@ -238,23 +337,14 @@ impl Live { |
| 238 | if accepting.stopped.load(Ordering::Acquire) { | 337 | if accepting.stopped.load(Ordering::Acquire) { |
| 239 | return; | 338 | return; |
| 240 | } | 339 | } |
| 241 | if let (Ok(stream), Some(tag)) = (stream, accepted.clone()) { | 340 | if let (Ok(stream), Some(tag)) = (stream, accepting.tag()) { |
| 242 | let shared = Arc::clone(&accepting); | 341 | let shared = Arc::clone(&accepting); |
| 243 | thread::spawn(move || shared.run(Arc::new(stream), Side::Responder, &tag)); | 342 | thread::spawn(move || shared.run(Arc::new(stream), Side::Responder, &tag)); |
| 244 | } | 343 | } |
| 245 | } | 344 | } |
| 246 | })?; | 345 | })?; |
| 247 | let daemon = match (reach, tag) { | 346 | shared.advertise(address.port()); |
| 248 | (Some(reach), Some(tag)) => { | 347 | Ok(Live { shared, address }) |
| 249 | Some(advertise(&shared, reach, tag, address.port()).map_err(io::Error::other)?) | ||
| 250 | } | ||
| 251 | _ => None, | ||
| 252 | }; | ||
| 253 | Ok(Live { | ||
| 254 | shared, | ||
| 255 | address, | ||
| 256 | daemon, | ||
| 257 | }) | ||
| 258 | } | 348 | } |
| 259 | 349 | ||
| 260 | /// Where this end listens. | 350 | /// Where this end listens. |
| ... | @@ -263,11 +353,28 @@ impl Live { | ... | @@ -263,11 +353,28 @@ impl Live { |
| 263 | } | 353 | } |
| 264 | 354 | ||
| 265 | /// The code others type to meet this end: the one it was given, or the one the relay | 355 | /// The code others type to meet this end: the one it was given, or the one the relay |
| 266 | /// numbered. | 356 | /// or this end numbered. |
| 267 | pub fn code(&self) -> Option<String> { | 357 | pub fn code(&self) -> Option<String> { |
| 268 | self.shared.state.lock().unwrap().code.clone() | 358 | self.shared.state.lock().unwrap().code.clone() |
| 269 | } | 359 | } |
| 270 | 360 | ||
| 361 | /// How the relay last answered. | ||
| 362 | pub fn relayed(&self) -> Relayed { | ||
| 363 | let state = self.shared.state.lock().unwrap(); | ||
| 364 | state.relayed.clone().unwrap_or(Relayed::Unknown) | ||
| 365 | } | ||
| 366 | |||
| 367 | /// Meetings that failed on the secret: wrong tries of this end's code, or this end's own | ||
| 368 | /// wrong code. | ||
| 369 | pub fn failed(&self) -> u32 { | ||
| 370 | self.shared.state.lock().unwrap().failed | ||
| 371 | } | ||
| 372 | |||
| 373 | /// Whether this end's code had too many wrong tries and admits no one new. | ||
| 374 | pub fn burned(&self) -> bool { | ||
| 375 | self.shared.state.lock().unwrap().burned | ||
| 376 | } | ||
| 377 | |||
| 271 | /// Connects to a peer at `address` that discovery did not find. | 378 | /// Connects to a peer at `address` that discovery did not find. |
| 272 | pub fn connect(&self, address: SocketAddr) { | 379 | pub fn connect(&self, address: SocketAddr) { |
| 273 | let shared = Arc::clone(&self.shared); | 380 | let shared = Arc::clone(&self.shared); |
| ... | @@ -283,7 +390,7 @@ impl Live { | ... | @@ -283,7 +390,7 @@ impl Live { |
| 283 | state.presence = presence; | 390 | state.presence = presence; |
| 284 | state.generation += 1; | 391 | state.generation += 1; |
| 285 | for link in state.peers.values() { | 392 | for link in state.peers.values() { |
| 286 | let _ = link.wake.send(()); | 393 | let _ = link.line.0.send(Out::Presence); |
| 287 | } | 394 | } |
| 288 | } | 395 | } |
| 289 | 396 | ||
| ... | @@ -292,17 +399,38 @@ impl Live { | ... | @@ -292,17 +399,38 @@ impl Live { |
| 292 | let state = self.shared.state.lock().unwrap(); | 399 | let state = self.shared.state.lock().unwrap(); |
| 293 | state.peers.values().map(|link| link.peer.clone()).collect() | 400 | state.peers.values().map(|link| link.peer.clone()).collect() |
| 294 | } | 401 | } |
| 402 | |||
| 403 | /// The line to `peer`, while it is connected. | ||
| 404 | pub fn line(&self, peer: &[u8; 16]) -> Option<Line> { | ||
| 405 | let state = self.shared.state.lock().unwrap(); | ||
| 406 | state.peers.get(peer).map(|link| link.line.clone()) | ||
| 407 | } | ||
| 408 | |||
| 409 | /// Says `reason` to every peer and leaves, waiting a moment for them to hear it. | ||
| 410 | pub fn leave(self, reason: &str) { | ||
| 411 | let bye = minicbor::to_vec(wire::Bye { | ||
| 412 | reason: reason.into(), | ||
| 413 | }) | ||
| 414 | .unwrap_or_default(); | ||
| 415 | for link in self.shared.state.lock().unwrap().peers.values() { | ||
| 416 | let _ = link.line.0.send(Out::Bye(bye.clone())); | ||
| 417 | } | ||
| 418 | let deadline = Instant::now() + Duration::from_secs(1); | ||
| 419 | while !self.shared.state.lock().unwrap().peers.is_empty() && Instant::now() < deadline { | ||
| 420 | thread::sleep(Duration::from_millis(20)); | ||
| 421 | } | ||
| 422 | } | ||
| 295 | } | 423 | } |
| 296 | 424 | ||
| 297 | impl Drop for Live { | 425 | impl Drop for Live { |
| 298 | fn drop(&mut self) { | 426 | fn drop(&mut self) { |
| 299 | self.shared.stopped.store(true, Ordering::Release); | 427 | self.shared.stopped.store(true, Ordering::Release); |
| 300 | if let Some(daemon) = &self.daemon { | ||
| 301 | let _ = daemon.shutdown(); | ||
| 302 | } | ||
| 303 | // Wakes the accepting thread to see it has stopped. | 428 | // Wakes the accepting thread to see it has stopped. |
| 304 | let _ = TcpStream::connect_timeout(&self.address, OPENING); | 429 | let _ = TcpStream::connect_timeout(&self.address, OPENING); |
| 305 | let state = self.shared.state.lock().unwrap(); | 430 | let mut state = self.shared.state.lock().unwrap(); |
| 431 | if let Some(daemon) = state.daemon.take() { | ||
| 432 | let _ = daemon.shutdown(); | ||
| 433 | } | ||
| 306 | for link in state.peers.values() { | 434 | for link in state.peers.values() { |
| 307 | link.pipe.shutdown(); | 435 | link.pipe.shutdown(); |
| 308 | } | 436 | } |
| ... | @@ -312,69 +440,34 @@ impl Drop for Live { | ... | @@ -312,69 +440,34 @@ impl Drop for Live { |
| 312 | } | 440 | } |
| 313 | } | 441 | } |
| 314 | 442 | ||
| 315 | /// Advertises `shared` as in room `tag` on `port` and connects to the peers in its room that | 443 | impl Shared { |
| 316 | /// discovery finds with a higher id than its own, which leave the connecting to it. | 444 | /// The room's tag as it stands: a code's, once numbered. |
| 317 | fn advertise( | 445 | fn tag(&self) -> Option<String> { |
| 318 | shared: &Arc<Shared>, | 446 | match &self.room { |
| 319 | reach: Reach, | 447 | Room::Code { .. } => code_parts(self.state.lock().unwrap().code.as_deref()?) |
| 320 | tag: String, | 448 | .0 |
| 321 | port: u16, | 449 | .map(|number| format!("code-{number}")), |
| 322 | ) -> mdns_sd::Result<ServiceDaemon> { | 450 | room => room.tag(), |
| 323 | let daemon = ServiceDaemon::new()?; | ||
| 324 | let id = hex(&shared.me.peer); | ||
| 325 | let properties = [("v", "1"), ("room", tag.as_str()), ("peer", id.as_str())]; | ||
| 326 | let host = format!("snowbound-{id}.local."); | ||
| 327 | let info = match reach { | ||
| 328 | Reach::Network => { | ||
| 329 | ServiceInfo::new(SERVICE, &id, &host, "", port, &properties[..])?.enable_addr_auto() | ||
| 330 | } | 451 | } |
| 331 | Reach::Loopback => { | 452 | } |
| 332 | daemon.disable_interface(IfKind::All)?; | 453 | |
| 333 | daemon.enable_interface(IfKind::LoopbackV4)?; | 454 | /// Advertises this end on `port` once its room has a tag, where its reach says, and |
| 334 | ServiceInfo::new( | 455 | /// connects to the peers in its room that discovery finds with a higher id than its own, |
| 335 | SERVICE, | 456 | /// which leave the connecting to it. |
| 336 | &id, | 457 | fn advertise(self: &Arc<Self>, port: u16) { |
| 337 | &host, | 458 | let (Some(reach), Some(tag)) = (self.reach, self.tag()) else { |
| 338 | IpAddr::V4(Ipv4Addr::LOCALHOST), | 459 | return; |
| 339 | port, | 460 | }; |
| 340 | &properties[..], | 461 | let mut state = self.state.lock().unwrap(); |
| 341 | )? | 462 | if state.daemon.is_some() || self.stopped.load(Ordering::Acquire) { |
| 463 | return; | ||
| 342 | } | 464 | } |
| 343 | }; | 465 | match discover(self, reach, tag, port) { |
| 344 | daemon.register(info)?; | 466 | Ok(daemon) => state.daemon = Some(daemon), |
| 345 | let found = daemon.browse(SERVICE)?; | 467 | Err(error) => eprintln!("Live: no discovery: {error}"), |
| 346 | let shared = Arc::clone(shared); | 468 | } |
| 347 | thread::Builder::new() | 469 | } |
| 348 | .name("live discovery".into()) | ||
| 349 | .spawn(move || { | ||
| 350 | while let Ok(event) = found.recv() { | ||
| 351 | let ServiceEvent::ServiceResolved(service) = event else { | ||
| 352 | continue; | ||
| 353 | }; | ||
| 354 | let (Some(room), Some(peer)) = ( | ||
| 355 | service.get_property_val_str("room"), | ||
| 356 | service.get_property_val_str("peer"), | ||
| 357 | ) else { | ||
| 358 | continue; | ||
| 359 | }; | ||
| 360 | if room != tag || peer <= id.as_str() || shared.knows(peer) { | ||
| 361 | continue; | ||
| 362 | } | ||
| 363 | let mut addresses: Vec<IpAddr> = | ||
| 364 | service.addresses.iter().map(|ip| ip.to_ip_addr()).collect(); | ||
| 365 | addresses.sort_by_key(|ip| (!ip.is_ipv4(), !ip.is_loopback())); | ||
| 366 | if let Some(ip) = addresses.first() { | ||
| 367 | let address = SocketAddr::new(*ip, service.port); | ||
| 368 | let shared = Arc::clone(&shared); | ||
| 369 | thread::spawn(move || shared.dial(address)); | ||
| 370 | } | ||
| 371 | } | ||
| 372 | }) | ||
| 373 | .map_err(|error| mdns_sd::Error::Msg(error.to_string()))?; | ||
| 374 | Ok(daemon) | ||
| 375 | } | ||
| 376 | 470 | ||
| 377 | impl Shared { | ||
| 378 | fn knows(&self, peer: &str) -> bool { | 471 | fn knows(&self, peer: &str) -> bool { |
| 379 | let state = self.state.lock().unwrap(); | 472 | let state = self.state.lock().unwrap(); |
| 380 | state.peers.keys().any(|id| hex(id) == peer) | 473 | state.peers.keys().any(|id| hex(id) == peer) |
| ... | @@ -383,11 +476,11 @@ impl Shared { | ... | @@ -383,11 +476,11 @@ impl Shared { |
| 383 | /// Connects to `address`, and again while the peer is there and the connection was the | 476 | /// Connects to `address`, and again while the peer is there and the connection was the |
| 384 | /// one this end kept. | 477 | /// one this end kept. |
| 385 | fn dial(self: Arc<Self>, address: SocketAddr) { | 478 | fn dial(self: Arc<Self>, address: SocketAddr) { |
| 386 | let Some(tag) = self.room.tag() else { | ||
| 387 | return; | ||
| 388 | }; | ||
| 389 | let mut wait = Duration::from_secs(1); | 479 | let mut wait = Duration::from_secs(1); |
| 390 | while let Ok(stream) = TcpStream::connect_timeout(&address, OPENING) { | 480 | while let Ok(stream) = TcpStream::connect_timeout(&address, OPENING) { |
| 481 | let Some(tag) = self.tag() else { | ||
| 482 | return; | ||
| 483 | }; | ||
| 391 | if !Arc::clone(&self).run(Arc::new(stream), Side::Initiator, &tag) | 484 | if !Arc::clone(&self).run(Arc::new(stream), Side::Initiator, &tag) |
| 392 | || self.stopped.load(Ordering::Acquire) | 485 | || self.stopped.load(Ordering::Acquire) |
| 393 | { | 486 | { |
| ... | @@ -398,10 +491,24 @@ impl Shared { | ... | @@ -398,10 +491,24 @@ impl Shared { |
| 398 | } | 491 | } |
| 399 | } | 492 | } |
| 400 | 493 | ||
| 494 | /// Counts a meeting that failed on the secret: the end sharing a code burns it after too | ||
| 495 | /// many, and an end that typed one gives up at once. | ||
| 496 | fn failed(&self) { | ||
| 497 | let mut state = self.state.lock().unwrap(); | ||
| 498 | state.failed += 1; | ||
| 499 | match &self.room { | ||
| 500 | Room::Code { owner: true, .. } if state.failed >= TRIES => state.burned = true, | ||
| 501 | Room::Code { owner: false, .. } => self.stopped.store(true, Ordering::Release), | ||
| 502 | _ => {} | ||
| 503 | } | ||
| 504 | drop(state); | ||
| 505 | (self.events)(Event::Changed); | ||
| 506 | } | ||
| 507 | |||
| 401 | /// Meets the peer at the other end of `pipe` in room `tag`, then reads from it until it | 508 | /// Meets the peer at the other end of `pipe` in room `tag`, then reads from it until it |
| 402 | /// goes: whether it was the connection kept to that peer. | 509 | /// goes: whether it was the connection kept to that peer. |
| 403 | fn run(self: Arc<Self>, pipe: Arc<dyn Pipe>, side: Side, tag: &str) -> bool { | 510 | fn run(self: Arc<Self>, pipe: Arc<dyn Pipe>, side: Side, tag: &str) -> bool { |
| 404 | if self.stopped.load(Ordering::Acquire) { | 511 | if self.stopped.load(Ordering::Acquire) || self.state.lock().unwrap().burned { |
| 405 | pipe.shutdown(); | 512 | pipe.shutdown(); |
| 406 | return false; | 513 | return false; |
| 407 | } | 514 | } |
| ... | @@ -424,14 +531,19 @@ impl Shared { | ... | @@ -424,14 +531,19 @@ impl Shared { |
| 424 | Ok(met) => met, | 531 | Ok(met) => met, |
| 425 | Err(error) => { | 532 | Err(error) => { |
| 426 | eprintln!("Live: no meeting in {tag}: {error}"); | 533 | eprintln!("Live: no meeting in {tag}: {error}"); |
| 534 | if error.kind() == io::ErrorKind::InvalidData { | ||
| 535 | self.failed(); | ||
| 536 | } | ||
| 427 | pipe.shutdown(); | 537 | pipe.shutdown(); |
| 428 | return false; | 538 | return false; |
| 429 | } | 539 | } |
| 430 | }; | 540 | }; |
| 431 | let peer = hello.peer; | 541 | let peer = hello.peer; |
| 432 | let name = hello.name.clone(); | 542 | let name = hello.name.clone(); |
| 543 | let hello = Arc::new(hello); | ||
| 433 | let connection = self.connections.fetch_add(1, Ordering::Relaxed); | 544 | let connection = self.connections.fetch_add(1, Ordering::Relaxed); |
| 434 | let (wake, woken) = mpsc::channel(); | 545 | let (out, outgoing) = mpsc::channel(); |
| 546 | let line = Line(out); | ||
| 435 | { | 547 | { |
| 436 | let mut state = self.state.lock().unwrap(); | 548 | let mut state = self.state.lock().unwrap(); |
| 437 | // A peer met both directly and through a relay keeps the direct connection, as | 549 | // A peer met both directly and through a relay keeps the direct connection, as |
| ... | @@ -450,39 +562,48 @@ impl Shared { | ... | @@ -450,39 +562,48 @@ impl Shared { |
| 450 | pipe.shutdown(); | 562 | pipe.shutdown(); |
| 451 | return false; | 563 | return false; |
| 452 | } | 564 | } |
| 453 | let _ = wake.send(()); | 565 | let _ = line.0.send(Out::Presence); |
| 454 | state.peers.insert( | 566 | state.peers.insert( |
| 455 | peer, | 567 | peer, |
| 456 | Link { | 568 | Link { |
| 457 | connection, | 569 | connection, |
| 458 | peer: Peer { | 570 | peer: Peer { |
| 459 | hello: Arc::new(hello), | 571 | hello: Arc::clone(&hello), |
| 460 | presence: None, | 572 | presence: None, |
| 461 | }, | 573 | }, |
| 462 | wake, | 574 | line: line.clone(), |
| 463 | pipe: Arc::clone(&pipe), | 575 | pipe: Arc::clone(&pipe), |
| 464 | }, | 576 | }, |
| 465 | ); | 577 | ); |
| 466 | } | 578 | } |
| 467 | (self.notify)(); | ||
| 468 | let writing = Arc::clone(&pipe); | 579 | let writing = Arc::clone(&pipe); |
| 469 | let shared = Arc::clone(&self); | 580 | let shared = Arc::clone(&self); |
| 470 | thread::spawn(move || shared.write(&*writing, send, woken)); | 581 | thread::spawn(move || shared.write(&*writing, send, outgoing)); |
| 582 | (self.events)(Event::Met(&hello, &line)); | ||
| 583 | (self.events)(Event::Changed); | ||
| 471 | let ended = loop { | 584 | let ended = loop { |
| 472 | let (message, body) = match receive.receive(&mut stream) { | 585 | let (message, body) = match receive.receive(&mut stream) { |
| 473 | Ok(frame) => frame, | 586 | Ok(frame) => frame, |
| 474 | Err(error) => break Some(error), | 587 | Err(error) => break Some(error), |
| 475 | }; | 588 | }; |
| 476 | if message != kind::PRESENCE { | 589 | match message { |
| 477 | continue; | 590 | kind::HELLO | kind::PING => {} |
| 478 | } | 591 | kind::PRESENCE => { |
| 479 | let Ok(presence) = minicbor::decode::<Presence>(&body) else { | 592 | let Ok(presence) = minicbor::decode::<Presence>(&body) else { |
| 480 | break None; | 593 | break None; |
| 481 | }; | 594 | }; |
| 482 | if let Some(link) = self.state.lock().unwrap().peers.get_mut(&peer) { | 595 | if let Some(link) = self.state.lock().unwrap().peers.get_mut(&peer) { |
| 483 | link.peer.presence = Some(presence); | 596 | link.peer.presence = Some(presence); |
| 597 | } | ||
| 598 | (self.events)(Event::Changed); | ||
| 599 | } | ||
| 600 | kind => (self.events)(Event::Frame { | ||
| 601 | from: &hello, | ||
| 602 | kind, | ||
| 603 | body: &body, | ||
| 604 | line: &line, | ||
| 605 | }), | ||
| 484 | } | 606 | } |
| 485 | (self.notify)(); | ||
| 486 | }; | 607 | }; |
| 487 | if let Some(error) = ended.filter(|error| error.kind() == io::ErrorKind::InvalidData) { | 608 | if let Some(error) = ended.filter(|error| error.kind() == io::ErrorKind::InvalidData) { |
| 488 | eprintln!("Live: the connection to {name} broke ({error}); meeting again"); | 609 | eprintln!("Live: the connection to {name} broke ({error}); meeting again"); |
| ... | @@ -497,18 +618,19 @@ impl Shared { | ... | @@ -497,18 +618,19 @@ impl Shared { |
| 497 | { | 618 | { |
| 498 | state.peers.remove(&peer); | 619 | state.peers.remove(&peer); |
| 499 | drop(state); | 620 | drop(state); |
| 500 | (self.notify)(); | 621 | (self.events)(Event::Left(&hello)); |
| 622 | (self.events)(Event::Changed); | ||
| 501 | } | 623 | } |
| 502 | true | 624 | true |
| 503 | } | 625 | } |
| 504 | 626 | ||
| 505 | /// Sends the newest presence whenever woken, and a ping when quiet. | 627 | /// Sends the newest presence whenever woken, frames as they come, and a ping when quiet. |
| 506 | fn write(&self, pipe: &dyn Pipe, mut send: Sealer, woken: mpsc::Receiver<()>) { | 628 | fn write(&self, pipe: &dyn Pipe, mut send: Sealer, outgoing: mpsc::Receiver<Out>) { |
| 507 | let mut stream = pipe; | 629 | let mut stream = pipe; |
| 508 | let mut sent = None; | 630 | let mut sent = None; |
| 509 | loop { | 631 | loop { |
| 510 | let result = match woken.recv_timeout(PING) { | 632 | let result = match outgoing.recv_timeout(PING) { |
| 511 | Ok(()) => { | 633 | Ok(Out::Presence) => { |
| 512 | let presence = { | 634 | let presence = { |
| 513 | let state = self.state.lock().unwrap(); | 635 | let state = self.state.lock().unwrap(); |
| 514 | if sent == Some(state.generation) { | 636 | if sent == Some(state.generation) { |
| ... | @@ -519,6 +641,12 @@ impl Shared { | ... | @@ -519,6 +641,12 @@ impl Shared { |
| 519 | }; | 641 | }; |
| 520 | send.send(&mut stream, kind::PRESENCE, &presence) | 642 | send.send(&mut stream, kind::PRESENCE, &presence) |
| 521 | } | 643 | } |
| 644 | Ok(Out::Frame(kind, body)) => send.send_encoded(&mut stream, kind, &body), | ||
| 645 | Ok(Out::Bye(body)) => { | ||
| 646 | let _ = send.send_encoded(&mut stream, kind::BYE, &body); | ||
| 647 | pipe.shutdown(); | ||
| 648 | return; | ||
| 649 | } | ||
| 522 | Err(mpsc::RecvTimeoutError::Timeout) => send.send(&mut stream, kind::PING, &()), | 650 | Err(mpsc::RecvTimeoutError::Timeout) => send.send(&mut stream, kind::PING, &()), |
| 523 | Err(mpsc::RecvTimeoutError::Disconnected) => return, | 651 | Err(mpsc::RecvTimeoutError::Disconnected) => return, |
| 524 | }; | 652 | }; |
| ... | @@ -530,6 +658,70 @@ impl Shared { | ... | @@ -530,6 +658,70 @@ impl Shared { |
| 530 | } | 658 | } |
| 531 | } | 659 | } |
| 532 | 660 | ||
| 661 | /// Advertises `shared` as in room `tag` on `port` and connects to the peers in its room that | ||
| 662 | /// discovery finds with a higher id than its own. | ||
| 663 | fn discover( | ||
| 664 | shared: &Arc<Shared>, | ||
| 665 | reach: Reach, | ||
| 666 | tag: String, | ||
| 667 | port: u16, | ||
| 668 | ) -> mdns_sd::Result<ServiceDaemon> { | ||
| 669 | let daemon = ServiceDaemon::new()?; | ||
| 670 | let id = hex(&shared.me.peer); | ||
| 671 | let properties = [("v", "1"), ("room", tag.as_str()), ("peer", id.as_str())]; | ||
| 672 | let host = format!("snowbound-{id}.local."); | ||
| 673 | let info = match reach { | ||
| 674 | Reach::Network => { | ||
| 675 | ServiceInfo::new(SERVICE, &id, &host, "", port, &properties[..])?.enable_addr_auto() | ||
| 676 | } | ||
| 677 | Reach::Loopback => { | ||
| 678 | daemon.disable_interface(IfKind::All)?; | ||
| 679 | daemon.enable_interface(IfKind::LoopbackV4)?; | ||
| 680 | ServiceInfo::new( | ||
| 681 | SERVICE, | ||
| 682 | &id, | ||
| 683 | &host, | ||
| 684 | IpAddr::V4(Ipv4Addr::LOCALHOST), | ||
| 685 | port, | ||
| 686 | &properties[..], | ||
| 687 | )? | ||
| 688 | } | ||
| 689 | }; | ||
| 690 | daemon.register(info)?; | ||
| 691 | let found = daemon.browse(SERVICE)?; | ||
| 692 | let shared = Arc::downgrade(shared); | ||
| 693 | thread::Builder::new() | ||
| 694 | .name("live discovery".into()) | ||
| 695 | .spawn(move || { | ||
| 696 | while let Ok(event) = found.recv() { | ||
| 697 | let ServiceEvent::ServiceResolved(service) = event else { | ||
| 698 | continue; | ||
| 699 | }; | ||
| 700 | let Some(shared) = shared.upgrade() else { | ||
| 701 | return; | ||
| 702 | }; | ||
| 703 | let (Some(room), Some(peer)) = ( | ||
| 704 | service.get_property_val_str("room"), | ||
| 705 | service.get_property_val_str("peer"), | ||
| 706 | ) else { | ||
| 707 | continue; | ||
| 708 | }; | ||
| 709 | if room != tag || peer <= id.as_str() || shared.knows(peer) { | ||
| 710 | continue; | ||
| 711 | } | ||
| 712 | let mut addresses: Vec<IpAddr> = | ||
| 713 | service.addresses.iter().map(|ip| ip.to_ip_addr()).collect(); | ||
| 714 | addresses.sort_by_key(|ip| (!ip.is_ipv4(), !ip.is_loopback())); | ||
| 715 | if let Some(ip) = addresses.first() { | ||
| 716 | let address = SocketAddr::new(*ip, service.port); | ||
| 717 | thread::spawn(move || shared.dial(address)); | ||
| 718 | } | ||
| 719 | } | ||
| 720 | }) | ||
| 721 | .map_err(|error| mdns_sd::Error::Msg(error.to_string()))?; | ||
| 722 | Ok(daemon) | ||
| 723 | } | ||
| 724 | |||
| 533 | fn hex(bytes: &[u8]) -> String { | 725 | fn hex(bytes: &[u8]) -> String { |
| 534 | bytes.iter().map(|byte| format!("{byte:02x}")).collect() | 726 | bytes.iter().map(|byte| format!("{byte:02x}")).collect() |
| 535 | } | 727 | } |
crates/notebook/src/live/relay.rs+40-22| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | //! the relay sees only who talks to whom, when, and how much. A stream that breaks makes this | 3 | //! the relay sees only who talks to whom, when, and how much. A stream that breaks makes this |
| 4 | //! end join the room again, which ends every stream it had there; peers then meet afresh. | 4 | //! end join the room again, which ends every stream it had there; peers then meet afresh. |
| 5 | 5 | ||
| 6 | use super::{OPENING, PATIENCE, Pipe, Shared, Side, code_parts}; | 6 | use super::{Event, OPENING, PATIENCE, Pipe, Relayed as Answer, Shared, Side, code_parts}; |
| 7 | use ::relay::{Notice, SLOT, Verdict, ws}; | 7 | use ::relay::{Notice, SLOT, Verdict, ws}; |
| 8 | use base64::Engine; | 8 | use base64::Engine; |
| 9 | use rustls::{ClientConfig, ClientConnection, RootCertStore, pki_types::ServerName}; | 9 | use rustls::{ClientConfig, ClientConnection, RootCertStore, pki_types::ServerName}; |
| ... | @@ -29,13 +29,14 @@ const MOST: usize = 1 << 20; | ... | @@ -29,13 +29,14 @@ const MOST: usize = 1 << 20; |
| 29 | 29 | ||
| 30 | type Reader = ws::Reader<BufReader<Box<dyn Read + Send>>>; | 30 | type Reader = ws::Reader<BufReader<Box<dyn Read + Send>>>; |
| 31 | 31 | ||
| 32 | /// Joins `shared`'s room at the relay at `url`, and keeps joining whenever it falls out. | 32 | /// Joins `shared`'s room at the relay at `url`, and keeps joining whenever it falls out; |
| 33 | pub(super) fn join(shared: &Arc<Shared>, url: &str) -> io::Result<()> { | 33 | /// `port` is where it listens, to advertise once the relay numbers its code. |
| 34 | pub(super) fn join(shared: &Arc<Shared>, url: &str, port: u16) -> io::Result<()> { | ||
| 34 | let address = parse(url)?; | 35 | let address = parse(url)?; |
| 35 | let shared = Arc::clone(shared); | 36 | let shared = Arc::clone(shared); |
| 36 | thread::Builder::new() | 37 | thread::Builder::new() |
| 37 | .name("live relay".into()) | 38 | .name("live relay".into()) |
| 38 | .spawn(move || keep(&shared, &address))?; | 39 | .spawn(move || keep(&shared, &address, port))?; |
| 39 | Ok(()) | 40 | Ok(()) |
| 40 | } | 41 | } |
| 41 | 42 | ||
| ... | @@ -87,41 +88,56 @@ impl From<io::Error> for Failure { | ... | @@ -87,41 +88,56 @@ impl From<io::Error> for Failure { |
| 87 | } | 88 | } |
| 88 | } | 89 | } |
| 89 | 90 | ||
| 91 | /// Records how the relay answered, telling `shared`'s events where it changed. | ||
| 92 | fn answered(shared: &Shared, answer: Answer) { | ||
| 93 | let mut state = shared.state.lock().unwrap(); | ||
| 94 | if state.relayed.as_ref() != Some(&answer) { | ||
| 95 | state.relayed = Some(answer); | ||
| 96 | drop(state); | ||
| 97 | (shared.events)(Event::Changed); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 90 | /// Stays in the room at `address`, joining again whenever the connection ends, until | 101 | /// Stays in the room at `address`, joining again whenever the connection ends, until |
| 91 | /// `shared` stops. | 102 | /// `shared` stops. |
| 92 | fn keep(shared: &Arc<Shared>, address: &Address) { | 103 | fn keep(shared: &Arc<Shared>, address: &Address, port: u16) { |
| 93 | let mut wait = Duration::from_secs(1); | 104 | let mut wait = Duration::from_secs(1); |
| 94 | // The number the relay gave this end's code, asked for again on joining again. | 105 | let owner = shared.room.owner(); |
| 95 | let mut nameplate = None; | 106 | // The number of this end's code, asked for again on joining again. |
| 107 | let mut nameplate = owner | ||
| 108 | .then(|| code_parts(shared.state.lock().unwrap().code.as_deref()?).0) | ||
| 109 | .flatten(); | ||
| 96 | while !shared.stopped.load(Ordering::Acquire) { | 110 | while !shared.stopped.load(Ordering::Acquire) { |
| 97 | let began = Instant::now(); | 111 | let began = Instant::now(); |
| 98 | let tag = shared.room.tag(); | 112 | let path = match (owner, shared.tag()) { |
| 99 | let path = match &tag { | 113 | (false, Some(tag)) => format!("{}/v1/room/{tag}", address.path), |
| 100 | Some(tag) => format!("{}/v1/room/{tag}", address.path), | 114 | (false, None) => return, |
| 101 | None => match nameplate { | 115 | (true, _) => match nameplate { |
| 102 | Some(number) => format!("{}/v1/claim?nameplate={number}", address.path), | 116 | Some(number) => format!("{}/v1/claim?nameplate={number}", address.path), |
| 103 | None => format!("{}/v1/claim", address.path), | 117 | None => format!("{}/v1/claim", address.path), |
| 104 | }, | 118 | }, |
| 105 | }; | 119 | }; |
| 106 | match connect(address, &path, tag.is_none()) { | 120 | match connect(address, &path, owner) { |
| 107 | Ok((socket, reader)) => { | 121 | Ok((socket, reader)) => { |
| 108 | shared.state.lock().unwrap().relay = Some(Arc::clone(&socket)); | 122 | shared.state.lock().unwrap().relay = Some(Arc::clone(&socket)); |
| 123 | answered(shared, Answer::Joined); | ||
| 109 | if !shared.stopped.load(Ordering::Acquire) { | 124 | if !shared.stopped.load(Ordering::Acquire) { |
| 110 | session(shared, &socket, reader, &mut nameplate); | 125 | session(shared, &socket, reader, &mut nameplate, port); |
| 111 | } | 126 | } |
| 112 | shared.state.lock().unwrap().relay = None; | 127 | shared.state.lock().unwrap().relay = None; |
| 113 | socket.hang_up(); | 128 | socket.hang_up(); |
| 114 | } | 129 | } |
| 115 | Err(Failure::Refused(410, _)) => { | ||
| 116 | eprintln!("Live: the code has expired; ask for a new one"); | ||
| 117 | return; | ||
| 118 | } | ||
| 119 | Err(Failure::Refused(status, retry)) => { | 130 | Err(Failure::Refused(status, retry)) => { |
| 120 | eprintln!("Live: the relay refused to let this end in ({status})"); | 131 | eprintln!("Live: the relay refused to let this end in ({status})"); |
| 132 | answered(shared, Answer::Refused(status, retry)); | ||
| 133 | if status == 410 { | ||
| 134 | return; | ||
| 135 | } | ||
| 121 | wait = wait.max(retry.unwrap_or_default()); | 136 | wait = wait.max(retry.unwrap_or_default()); |
| 122 | } | 137 | } |
| 123 | Err(Failure::Network(error)) => { | 138 | Err(Failure::Network(error)) => { |
| 124 | eprintln!("Live: no relay at {}: {error}", address.authority); | 139 | eprintln!("Live: no relay at {}: {error}", address.authority); |
| 140 | answered(shared, Answer::Unreachable); | ||
| 125 | } | 141 | } |
| 126 | } | 142 | } |
| 127 | if began.elapsed() >= STEADY { | 143 | if began.elapsed() >= STEADY { |
| ... | @@ -337,8 +353,9 @@ fn session( | ... | @@ -337,8 +353,9 @@ fn session( |
| 337 | socket: &Arc<Socket>, | 353 | socket: &Arc<Socket>, |
| 338 | mut reader: Reader, | 354 | mut reader: Reader, |
| 339 | nameplate: &mut Option<u32>, | 355 | nameplate: &mut Option<u32>, |
| 356 | port: u16, | ||
| 340 | ) { | 357 | ) { |
| 341 | let mut tag = shared.room.tag(); | 358 | let mut tag = shared.tag(); |
| 342 | // Pings while the session lasts: dropping `_beat` at its end stops them. | 359 | // Pings while the session lasts: dropping `_beat` at its end stops them. |
| 343 | let (_beat, beats) = mpsc::channel::<()>(); | 360 | let (_beat, beats) = mpsc::channel::<()>(); |
| 344 | let pinging = Arc::clone(socket); | 361 | let pinging = Arc::clone(socket); |
| ... | @@ -355,16 +372,16 @@ fn session( | ... | @@ -355,16 +372,16 @@ fn session( |
| 355 | Ok(Notice::Nameplate(number)) => { | 372 | Ok(Notice::Nameplate(number)) => { |
| 356 | *nameplate = Some(number); | 373 | *nameplate = Some(number); |
| 357 | tag = Some(format!("code-{number}")); | 374 | tag = Some(format!("code-{number}")); |
| 358 | let super::Room::Code(words) = &shared.room else { | 375 | let super::Room::Code { code: words, .. } = &shared.room else { |
| 359 | continue; | 376 | continue; |
| 360 | }; | 377 | }; |
| 361 | let code = format!("{number}-{}", code_parts(words).1); | 378 | let code = format!("{number}-{}", code_parts(words).1); |
| 362 | let mut state = shared.state.lock().unwrap(); | 379 | let mut state = shared.state.lock().unwrap(); |
| 363 | if state.code.as_ref() != Some(&code) { | 380 | if state.code.as_ref() != Some(&code) { |
| 364 | eprintln!("Live: the code is {code}"); | ||
| 365 | state.code = Some(code); | 381 | state.code = Some(code); |
| 366 | drop(state); | 382 | drop(state); |
| 367 | (shared.notify)(); | 383 | shared.advertise(port); |
| 384 | (shared.events)(Event::Changed); | ||
| 368 | } | 385 | } |
| 369 | } | 386 | } |
| 370 | Ok(Notice::Welcome { you, members }) => { | 387 | Ok(Notice::Welcome { you, members }) => { |
| ... | @@ -378,7 +395,8 @@ fn session( | ... | @@ -378,7 +395,8 @@ fn session( |
| 378 | Ok(Notice::Burned) => { | 395 | Ok(Notice::Burned) => { |
| 379 | // Coming back, ask for another number. | 396 | // Coming back, ask for another number. |
| 380 | *nameplate = None; | 397 | *nameplate = None; |
| 381 | eprintln!("Live: too many wrong tries; the code admits no one new"); | 398 | shared.state.lock().unwrap().burned = true; |
| 399 | (shared.events)(Event::Changed); | ||
| 382 | } | 400 | } |
| 383 | Err(()) => {} | 401 | Err(()) => {} |
| 384 | }, | 402 | }, |
crates/notebook/src/live/share.rs created+1342| ... | @@ -0,0 +1,1342 @@ | ||
| 1 | //! Live Share: a notebook one Snowbound holds, opened on others through a short code. The | ||
| 2 | //! host serves its notebook's storage verbs (`session::Storage`) to each peer in the share's | ||
| 3 | //! room; a guest runs the replica, queue and merge it runs on an SMB share against those | ||
| 4 | //! verbs, so offline queueing, rebases and conflict pages work as there, and the host's files | ||
| 5 | //! stay what its own storage writes. A guest first meets the host in the code's room, where | ||
| 6 | //! the host welcomes it with the share's room and secret; a new share has a new secret, so | ||
| 7 | //! stopping retires every guest. Large bodies travel a chunk at a time, each answered before | ||
| 8 | //! the next, so a relay never holds much for a slow peer. | ||
| 9 | //! | ||
| 10 | //! The code's words come from the EFF's short word list | ||
| 11 | //! (<https://www.eff.org/dice>, CC BY 3.0 US), `yo-yo` replaced by `yarn`. | ||
| 12 | |||
| 13 | use super::{ | ||
| 14 | Event, Hello, Line, Live, Peer, Presence, Reach, Relayed, Room, | ||
| 15 | wire::{self, Failure, Reply, Request, Touched, Welcome, WireEntry, WireStamp, kind}, | ||
| 16 | }; | ||
| 17 | use crate::{Error, Result, background::Reports, discover, session::Storage}; | ||
| 18 | use onestore::{CommitError, CommitState, RevisionIndex, Stamp, Store, Transaction}; | ||
| 19 | use std::{ | ||
| 20 | collections::{BTreeMap, HashMap}, | ||
| 21 | io, | ||
| 22 | path::PathBuf, | ||
| 23 | sync::{ | ||
| 24 | Arc, Condvar, Mutex, | ||
| 25 | atomic::{AtomicBool, AtomicU64, Ordering}, | ||
| 26 | mpsc, | ||
| 27 | }, | ||
| 28 | thread, | ||
| 29 | time::{Duration, Instant}, | ||
| 30 | }; | ||
| 31 | |||
| 32 | /// The most bytes one message of a read or an upload carries. | ||
| 33 | const CHUNK: usize = 128 << 10; | ||
| 34 | /// The most bytes of chunks a guest has asked for and not yet been given. | ||
| 35 | const WINDOW: usize = 512 << 10; | ||
| 36 | /// How long a request waits for its reply. | ||
| 37 | const TIMEOUT: Duration = Duration::from_secs(60); | ||
| 38 | /// The largest file read or written whole. | ||
| 39 | const LIMIT: usize = 256 << 20; | ||
| 40 | /// Snapshots of files being read, per guest, and how long one is kept unread. | ||
| 41 | const SNAPSHOTS: usize = 8; | ||
| 42 | const SNAPSHOT_AGE: Duration = Duration::from_secs(120); | ||
| 43 | /// Sections whose image a host keeps to check guests' commits on. | ||
| 44 | const IMAGES: usize = 4; | ||
| 45 | /// What a host says leaving as it stops sharing. | ||
| 46 | const STOPPED: &str = "stopped"; | ||
| 47 | |||
| 48 | const WORDS: &str = include_str!("words.txt"); | ||
| 49 | |||
| 50 | /// Two random words for a code, `violet-otter`. | ||
| 51 | pub fn words() -> io::Result<String> { | ||
| 52 | let list: Vec<&str> = WORDS.lines().collect(); | ||
| 53 | let mut bytes = [0; 8]; | ||
| 54 | getrandom::fill(&mut bytes).map_err(|_| io::Error::other("System random source failed"))?; | ||
| 55 | let [first, second] = [&bytes[..4], &bytes[4..]] | ||
| 56 | .map(|bytes| u32::from_le_bytes(bytes.try_into().expect("4 bytes")) as usize); | ||
| 57 | let first = first % list.len(); | ||
| 58 | let mut second = second % (list.len() - 1); | ||
| 59 | if second >= first { | ||
| 60 | second += 1; | ||
| 61 | } | ||
| 62 | Ok(format!("{}-{}", list[first], list[second])) | ||
| 63 | } | ||
| 64 | |||
| 65 | /// A code as typed, `412 Violet otter`, in the form it is met by, `412-violet-otter`; none | ||
| 66 | /// where it is not a number and two words. | ||
| 67 | pub fn code(typed: &str) -> Option<String> { | ||
| 68 | let parts: Vec<String> = typed | ||
| 69 | .split(|c: char| c.is_whitespace() || c == '-') | ||
| 70 | .filter(|part| !part.is_empty()) | ||
| 71 | .map(str::to_lowercase) | ||
| 72 | .collect(); | ||
| 73 | match &parts[..] { | ||
| 74 | [number, first, second] | ||
| 75 | if number.parse::<u32>().is_ok() | ||
| 76 | && [first, second] | ||
| 77 | .iter() | ||
| 78 | .all(|word| word.chars().all(|c| c.is_ascii_lowercase())) => | ||
| 79 | { | ||
| 80 | Some(parts.join("-")) | ||
| 81 | } | ||
| 82 | _ => None, | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | /// What a host keeps of a share to take it up again after a relaunch. | ||
| 87 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] | ||
| 88 | pub struct Sharing { | ||
| 89 | pub share: [u8; 16], | ||
| 90 | /// The share room's secret, which every guest welcomed holds. | ||
| 91 | pub secret: [u8; 16], | ||
| 92 | /// The code: its words, with its number in front once one was given. | ||
| 93 | pub code: String, | ||
| 94 | pub password: String, | ||
| 95 | } | ||
| 96 | |||
| 97 | impl Sharing { | ||
| 98 | /// A new share: its own id and secret, and new words. | ||
| 99 | pub fn new(password: &str) -> io::Result<Self> { | ||
| 100 | let mut random = [0; 32]; | ||
| 101 | getrandom::fill(&mut random) | ||
| 102 | .map_err(|_| io::Error::other("System random source failed"))?; | ||
| 103 | Ok(Self { | ||
| 104 | share: random[..16].try_into().expect("16 bytes"), | ||
| 105 | secret: random[16..].try_into().expect("16 bytes"), | ||
| 106 | code: words()?, | ||
| 107 | password: password.to_owned(), | ||
| 108 | }) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | /// Where a guest keeps a share's notebook: `live://` and the share's id. | ||
| 113 | pub fn location(share: &[u8; 16]) -> String { | ||
| 114 | format!("live://{}", super::hex(share)) | ||
| 115 | } | ||
| 116 | |||
| 117 | /// Why joining failed. | ||
| 118 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
| 119 | pub enum Refusal { | ||
| 120 | /// Not a number and two words. | ||
| 121 | Malformed, | ||
| 122 | /// The code's words or password are wrong. | ||
| 123 | Wrong, | ||
| 124 | /// No one shares with the code's number now. | ||
| 125 | NoOne, | ||
| 126 | /// The code had too many wrong tries. | ||
| 127 | Expired, | ||
| 128 | /// Too many wrong codes from this network; try again after the wait, where known. | ||
| 129 | TooMany(Option<Duration>), | ||
| 130 | /// The relay is full. | ||
| 131 | Busy, | ||
| 132 | /// The relay couldn't be reached, and no one answered on this network. | ||
| 133 | Unreachable, | ||
| 134 | /// The relay let this end in, but no one answered. | ||
| 135 | TimedOut, | ||
| 136 | } | ||
| 137 | |||
| 138 | /// Meets the host sharing `code` (and `password`) as `me`, on the networks `reach` names and | ||
| 139 | /// through `relay`: the share it welcomes this end to. | ||
| 140 | pub fn join( | ||
| 141 | me: Hello, | ||
| 142 | code: &str, | ||
| 143 | password: &str, | ||
| 144 | reach: Option<Reach>, | ||
| 145 | relay: Option<&str>, | ||
| 146 | ) -> std::result::Result<Welcome, Refusal> { | ||
| 147 | let code = self::code(code).ok_or(Refusal::Malformed)?; | ||
| 148 | let (welcomed, welcome) = mpsc::channel(); | ||
| 149 | let (changed, waiting) = mpsc::channel(); | ||
| 150 | let live = Live::start( | ||
| 151 | me, | ||
| 152 | &Room::join(&code, password), | ||
| 153 | reach, | ||
| 154 | relay, | ||
| 155 | move |event| match event { | ||
| 156 | Event::Frame { | ||
| 157 | kind: kind::WELCOME, | ||
| 158 | body, | ||
| 159 | .. | ||
| 160 | } => { | ||
| 161 | if let Ok(body) = minicbor::decode::<Welcome>(body) { | ||
| 162 | let _ = welcomed.send(body); | ||
| 163 | } | ||
| 164 | } | ||
| 165 | _ => { | ||
| 166 | let _ = changed.send(()); | ||
| 167 | } | ||
| 168 | }, | ||
| 169 | ) | ||
| 170 | .map_err(|_| Refusal::Unreachable)?; | ||
| 171 | let start = Instant::now(); | ||
| 172 | loop { | ||
| 173 | if let Ok(welcome) = welcome.try_recv() { | ||
| 174 | return Ok(welcome); | ||
| 175 | } | ||
| 176 | if live.failed() > 0 { | ||
| 177 | return Err(Refusal::Wrong); | ||
| 178 | } | ||
| 179 | // A peer on this network may yet answer what the relay refused. | ||
| 180 | let waited = start.elapsed(); | ||
| 181 | let settled = waited > Duration::from_secs(3) || reach.is_none(); | ||
| 182 | match live.relayed() { | ||
| 183 | Relayed::Refused(404, _) if settled => return Err(Refusal::NoOne), | ||
| 184 | Relayed::Refused(410, _) => return Err(Refusal::Expired), | ||
| 185 | Relayed::Refused(429, wait) => return Err(Refusal::TooMany(wait)), | ||
| 186 | Relayed::Refused(503, _) if settled => return Err(Refusal::Busy), | ||
| 187 | Relayed::Unreachable if waited > Duration::from_secs(10) => { | ||
| 188 | return Err(Refusal::Unreachable); | ||
| 189 | } | ||
| 190 | Relayed::Unknown if relay.is_none() && waited > Duration::from_secs(10) => { | ||
| 191 | return Err(Refusal::NoOne); | ||
| 192 | } | ||
| 193 | _ if waited > Duration::from_secs(20) => return Err(Refusal::TimedOut), | ||
| 194 | _ => {} | ||
| 195 | } | ||
| 196 | let _ = waiting.recv_timeout(Duration::from_millis(100)); | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | /// A notebook shared while it lives: the share's room, serving the notebook's storage to the | ||
| 201 | /// guests in it, and the code's room, welcoming whoever knows the code. | ||
| 202 | pub struct Host { | ||
| 203 | me: Hello, | ||
| 204 | notebook: String, | ||
| 205 | reach: Option<Reach>, | ||
| 206 | relay: Option<String>, | ||
| 207 | sharing: Mutex<Sharing>, | ||
| 208 | room: Live, | ||
| 209 | pairing: Mutex<Live>, | ||
| 210 | served: Arc<Served>, | ||
| 211 | events: Arc<dyn Fn() + Send + Sync>, | ||
| 212 | } | ||
| 213 | |||
| 214 | impl Host { | ||
| 215 | /// Shares `storage`, the notebook named `notebook`, as `sharing` says, as `me`, where | ||
| 216 | /// `reach` and `relay` say. `events` runs on a network thread whenever the guests or the | ||
| 217 | /// code change. What guests change reaches the host as its own watch on the notebook's | ||
| 218 | /// folder reports it, and reaches the other guests at once. | ||
| 219 | pub fn start( | ||
| 220 | storage: Box<dyn Storage>, | ||
| 221 | me: Hello, | ||
| 222 | sharing: Sharing, | ||
| 223 | notebook: &str, | ||
| 224 | reach: Option<Reach>, | ||
| 225 | relay: Option<&str>, | ||
| 226 | events: impl Fn() + Send + Sync + 'static, | ||
| 227 | ) -> io::Result<Self> { | ||
| 228 | let events: Arc<dyn Fn() + Send + Sync> = Arc::new(events); | ||
| 229 | let served = Arc::new(Served { | ||
| 230 | storage, | ||
| 231 | images: Mutex::default(), | ||
| 232 | snapshots: Mutex::default(), | ||
| 233 | puts: Mutex::default(), | ||
| 234 | lines: Mutex::default(), | ||
| 235 | }); | ||
| 236 | let serving = Hello { | ||
| 237 | serves: Some(sharing.share), | ||
| 238 | ..me.clone() | ||
| 239 | }; | ||
| 240 | let (heard, told) = (Arc::clone(&served), Arc::clone(&events)); | ||
| 241 | let room = Live::start( | ||
| 242 | serving, | ||
| 243 | &Room::Notebook(sharing.secret), | ||
| 244 | reach, | ||
| 245 | relay, | ||
| 246 | move |event| match event { | ||
| 247 | Event::Met(hello, line) => { | ||
| 248 | heard.lines.lock().unwrap().insert(hello.peer, line.clone()); | ||
| 249 | } | ||
| 250 | Event::Left(hello) => heard.forget(&hello.peer), | ||
| 251 | Event::Frame { | ||
| 252 | from, | ||
| 253 | kind, | ||
| 254 | body, | ||
| 255 | line, | ||
| 256 | } if wire::KNOWN.contains(&kind) && kind > 256 && kind != kind::REPLY => { | ||
| 257 | let (served, line, peer) = (Arc::clone(&heard), line.clone(), from.peer); | ||
| 258 | let body = body.to_vec(); | ||
| 259 | thread::spawn(move || { | ||
| 260 | let reply = served.handle(&peer, kind, &body); | ||
| 261 | let _ = line.send(kind::REPLY, &reply); | ||
| 262 | }); | ||
| 263 | } | ||
| 264 | Event::Changed => told(), | ||
| 265 | _ => {} | ||
| 266 | }, | ||
| 267 | )?; | ||
| 268 | let host = Self { | ||
| 269 | notebook: notebook.to_owned(), | ||
| 270 | reach, | ||
| 271 | relay: relay.map(str::to_owned), | ||
| 272 | pairing: Mutex::new(pair(&me, &sharing, notebook, reach, relay, &events)?), | ||
| 273 | me, | ||
| 274 | sharing: Mutex::new(sharing), | ||
| 275 | room, | ||
| 276 | served, | ||
| 277 | events, | ||
| 278 | }; | ||
| 279 | Ok(host) | ||
| 280 | } | ||
| 281 | |||
| 282 | /// The code guests type, once it has its number. A code with too many wrong tries is | ||
| 283 | /// replaced by one with new words. | ||
| 284 | pub fn code(&self) -> Option<String> { | ||
| 285 | let mut pairing = self.pairing.lock().unwrap(); | ||
| 286 | if pairing.burned() { | ||
| 287 | let mut sharing = self.sharing.lock().unwrap(); | ||
| 288 | sharing.code = words().ok()?; | ||
| 289 | let relay = self.relay.as_deref(); | ||
| 290 | *pairing = pair( | ||
| 291 | &self.me, | ||
| 292 | &sharing, | ||
| 293 | &self.notebook, | ||
| 294 | self.reach, | ||
| 295 | relay, | ||
| 296 | &self.events, | ||
| 297 | ) | ||
| 298 | .ok()?; | ||
| 299 | } | ||
| 300 | let code = pairing.code(); | ||
| 301 | if let Some(code) = &code { | ||
| 302 | self.sharing.lock().unwrap().code = code.clone(); | ||
| 303 | } | ||
| 304 | code | ||
| 305 | } | ||
| 306 | |||
| 307 | /// The share as it stands, to take up again after a relaunch. | ||
| 308 | pub fn sharing(&self) -> Sharing { | ||
| 309 | self.code(); | ||
| 310 | self.sharing.lock().unwrap().clone() | ||
| 311 | } | ||
| 312 | |||
| 313 | /// How the relay last answered the code's room. | ||
| 314 | pub fn relayed(&self) -> Relayed { | ||
| 315 | self.pairing.lock().unwrap().relayed() | ||
| 316 | } | ||
| 317 | |||
| 318 | /// The peers in the share's room. | ||
| 319 | pub fn guests(&self) -> Vec<Peer> { | ||
| 320 | self.room.peers() | ||
| 321 | } | ||
| 322 | |||
| 323 | pub fn set_presence(&self, presence: Presence) { | ||
| 324 | self.room.set_presence(presence); | ||
| 325 | } | ||
| 326 | |||
| 327 | /// Tells every guest the files at these catalog paths changed. | ||
| 328 | pub fn touched(&self, paths: &[String]) { | ||
| 329 | self.served.tell(paths); | ||
| 330 | } | ||
| 331 | |||
| 332 | /// Stops sharing: every guest hears so and is let go. | ||
| 333 | pub fn stop(self) { | ||
| 334 | drop(self.pairing); | ||
| 335 | self.room.leave(STOPPED); | ||
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | /// The code's room, welcoming whoever knows the code to the share. | ||
| 340 | fn pair( | ||
| 341 | me: &Hello, | ||
| 342 | sharing: &Sharing, | ||
| 343 | notebook: &str, | ||
| 344 | reach: Option<Reach>, | ||
| 345 | relay: Option<&str>, | ||
| 346 | events: &Arc<dyn Fn() + Send + Sync>, | ||
| 347 | ) -> io::Result<Live> { | ||
| 348 | let welcome = Welcome { | ||
| 349 | share: sharing.share, | ||
| 350 | secret: sharing.secret, | ||
| 351 | notebook: notebook.to_owned(), | ||
| 352 | host: me.name.clone(), | ||
| 353 | }; | ||
| 354 | let told = Arc::clone(events); | ||
| 355 | Live::start( | ||
| 356 | Hello { | ||
| 357 | serves: None, | ||
| 358 | ..me.clone() | ||
| 359 | }, | ||
| 360 | &Room::share(&sharing.code, &sharing.password), | ||
| 361 | reach, | ||
| 362 | relay, | ||
| 363 | move |event| match event { | ||
| 364 | Event::Met(_, line) => { | ||
| 365 | let _ = line.send(kind::WELCOME, &welcome); | ||
| 366 | } | ||
| 367 | Event::Changed => told(), | ||
| 368 | _ => {} | ||
| 369 | }, | ||
| 370 | ) | ||
| 371 | } | ||
| 372 | |||
| 373 | /// A host's side of its guests' storage requests. | ||
| 374 | struct Served { | ||
| 375 | storage: Box<dyn Storage>, | ||
| 376 | /// Sections' images by path, with their stamps, to check commits on. | ||
| 377 | images: Mutex<Vec<Image>>, | ||
| 378 | /// Files being read a chunk at a time, by guest and the read's first request. | ||
| 379 | snapshots: Mutex<ByGuest<(Arc<Vec<u8>>, Instant)>>, | ||
| 380 | /// Bytes a later request carries, by guest and upload. | ||
| 381 | puts: Mutex<ByGuest<Vec<u8>>>, | ||
| 382 | lines: Mutex<BTreeMap<[u8; 16], Line>>, | ||
| 383 | } | ||
| 384 | |||
| 385 | /// A section's path, stamp and image. | ||
| 386 | type Image = (String, Stamp, Arc<Vec<u8>>); | ||
| 387 | /// What a guest's requests left, by guest and request. | ||
| 388 | type ByGuest<T> = HashMap<([u8; 16], u64), T>; | ||
| 389 | |||
| 390 | /// Whether a guest may name `path`: a catalog path inside the notebook, and not presence's | ||
| 391 | /// own secret, which guests meet in the share's room instead. | ||
| 392 | fn allowed(path: &str) -> bool { | ||
| 393 | path.split('/').all(|part| { | ||
| 394 | !part.is_empty() && part != "." && part != ".." && !part.contains(['\\', '\0', ':']) | ||
| 395 | }) && !path.to_ascii_lowercase().starts_with(".snowbound/live") | ||
| 396 | } | ||
| 397 | |||
| 398 | /// The folder holding catalog path `path`. | ||
| 399 | fn folder(path: &str) -> String { | ||
| 400 | path.rsplit_once('/') | ||
| 401 | .map_or(String::new(), |(folder, _)| folder.to_owned()) | ||
| 402 | } | ||
| 403 | |||
| 404 | fn refused(kind: io::ErrorKind, message: &str) -> Error { | ||
| 405 | io::Error::new(kind, message.to_owned()).into() | ||
| 406 | } | ||
| 407 | |||
| 408 | impl Served { | ||
| 409 | fn forget(&self, peer: &[u8; 16]) { | ||
| 410 | self.lines.lock().unwrap().remove(peer); | ||
| 411 | self.snapshots | ||
| 412 | .lock() | ||
| 413 | .unwrap() | ||
| 414 | .retain(|(guest, _), _| guest != peer); | ||
| 415 | self.puts | ||
| 416 | .lock() | ||
| 417 | .unwrap() | ||
| 418 | .retain(|(guest, _), _| guest != peer); | ||
| 419 | } | ||
| 420 | |||
| 421 | /// Tells every guest the files at `paths` changed. | ||
| 422 | fn tell(&self, paths: &[String]) { | ||
| 423 | let touched = Touched { | ||
| 424 | paths: paths.to_vec(), | ||
| 425 | }; | ||
| 426 | for line in self.lines.lock().unwrap().values() { | ||
| 427 | let _ = line.send(kind::TOUCHED, &touched); | ||
| 428 | } | ||
| 429 | } | ||
| 430 | |||
| 431 | fn handle(&self, peer: &[u8; 16], kind: u16, body: &[u8]) -> Reply { | ||
| 432 | let request = match minicbor::decode::<Request>(body) { | ||
| 433 | Ok(request) => request, | ||
| 434 | Err(_) => { | ||
| 435 | return failed( | ||
| 436 | 0, | ||
| 437 | refused(io::ErrorKind::InvalidData, "A malformed request"), | ||
| 438 | ); | ||
| 439 | } | ||
| 440 | }; | ||
| 441 | let id = request.id; | ||
| 442 | match self.answer(peer, kind, request) { | ||
| 443 | Ok(reply) => Reply { id, ..reply }, | ||
| 444 | Err(error) => failed(id, error), | ||
| 445 | } | ||
| 446 | } | ||
| 447 | |||
| 448 | fn answer(&self, peer: &[u8; 16], kind: u16, request: Request) -> Result<Reply> { | ||
| 449 | let path = request.path.as_str(); | ||
| 450 | if !(path.is_empty() && matches!(kind, kind::LIST | kind::PUT) || allowed(path)) | ||
| 451 | || request.to.as_deref().is_some_and(|to| !allowed(to)) | ||
| 452 | { | ||
| 453 | return Err(refused( | ||
| 454 | io::ErrorKind::PermissionDenied, | ||
| 455 | "Outside the notebook", | ||
| 456 | )); | ||
| 457 | } | ||
| 458 | let to = || { | ||
| 459 | request | ||
| 460 | .to | ||
| 461 | .as_deref() | ||
| 462 | .ok_or_else(|| refused(io::ErrorKind::InvalidInput, "No target")) | ||
| 463 | }; | ||
| 464 | let stamp = || -> Result<Stamp> { | ||
| 465 | Ok(request | ||
| 466 | .stamp | ||
| 467 | .as_ref() | ||
| 468 | .ok_or_else(|| refused(io::ErrorKind::InvalidInput, "No stamp"))? | ||
| 469 | .try_into()?) | ||
| 470 | }; | ||
| 471 | let done = Reply::default(); | ||
| 472 | Ok(match kind { | ||
| 473 | kind::LIST => Reply { | ||
| 474 | entries: Some( | ||
| 475 | self.storage | ||
| 476 | .entries(path)? | ||
| 477 | .into_iter() | ||
| 478 | .filter(|entry| allowed(&within(path, &entry.name))) | ||
| 479 | .map(|entry| wire_entry(&entry)) | ||
| 480 | .collect(), | ||
| 481 | ), | ||
| 482 | ..done | ||
| 483 | }, | ||
| 484 | kind::STAMP => Reply { | ||
| 485 | stamp: Some((&self.storage.stamp(path)?).into()), | ||
| 486 | ..done | ||
| 487 | }, | ||
| 488 | kind::EXISTS => Reply { | ||
| 489 | exists: Some(self.storage.exists(path)), | ||
| 490 | ..done | ||
| 491 | }, | ||
| 492 | kind::READ | kind::READ_FILE => self.read(peer, kind, &request)?, | ||
| 493 | kind::PUT => { | ||
| 494 | let handle = request.handle.unwrap_or_default(); | ||
| 495 | let bytes = request.bytes.unwrap_or_default(); | ||
| 496 | let mut puts = self.puts.lock().unwrap(); | ||
| 497 | let held = puts.entry((*peer, handle)).or_default(); | ||
| 498 | if request.offset != Some(held.len() as u64) || held.len() + bytes.len() > LIMIT { | ||
| 499 | return Err(refused( | ||
| 500 | io::ErrorKind::InvalidInput, | ||
| 501 | "An upload out of order", | ||
| 502 | )); | ||
| 503 | } | ||
| 504 | held.extend_from_slice(&bytes); | ||
| 505 | done | ||
| 506 | } | ||
| 507 | kind::COMMIT => { | ||
| 508 | let transaction = Transaction::from_bytes(&self.carried(peer, &request)?)?; | ||
| 509 | self.commit(path, &transaction)?; | ||
| 510 | self.tell(&[path.to_owned()]); | ||
| 511 | done | ||
| 512 | } | ||
| 513 | kind::CONFIRM => { | ||
| 514 | self.storage.confirm(path, &stamp()?)?; | ||
| 515 | done | ||
| 516 | } | ||
| 517 | kind::CREATE => { | ||
| 518 | self.storage.create(path, &self.carried(peer, &request)?)?; | ||
| 519 | self.tell(&[folder(path)]); | ||
| 520 | done | ||
| 521 | } | ||
| 522 | kind::CREATE_DIRECTORY => { | ||
| 523 | self.storage.create_directory(path)?; | ||
| 524 | self.tell(&[folder(path)]); | ||
| 525 | done | ||
| 526 | } | ||
| 527 | kind::HIDE => { | ||
| 528 | self.storage.hide(path)?; | ||
| 529 | done | ||
| 530 | } | ||
| 531 | kind::RENAME | kind::REPLACE => { | ||
| 532 | let to = to()?; | ||
| 533 | if kind == kind::RENAME { | ||
| 534 | self.storage.rename(path, to)?; | ||
| 535 | } else { | ||
| 536 | self.storage.replace(path, to)?; | ||
| 537 | } | ||
| 538 | self.tell(&[folder(path), folder(to)]); | ||
| 539 | done | ||
| 540 | } | ||
| 541 | kind::DELETE => { | ||
| 542 | self.storage.delete(path)?; | ||
| 543 | self.tell(&[folder(path)]); | ||
| 544 | done | ||
| 545 | } | ||
| 546 | kind::PLACE => { | ||
| 547 | let ancestor = request | ||
| 548 | .ancestor | ||
| 549 | .ok_or_else(|| refused(io::ErrorKind::InvalidInput, "No ancestor"))?; | ||
| 550 | let name = request.name.as_deref().unwrap_or_default(); | ||
| 551 | self.storage.place(path, ancestor, name)?; | ||
| 552 | self.tell(&[path.to_owned()]); | ||
| 553 | done | ||
| 554 | } | ||
| 555 | kind::SUPERSEDE => { | ||
| 556 | self.storage.supersede(path, &stamp()?, to()?)?; | ||
| 557 | self.tell(&[folder(path)]); | ||
| 558 | done | ||
| 559 | } | ||
| 560 | _ => return Err(refused(io::ErrorKind::Unsupported, "An unknown request")), | ||
| 561 | }) | ||
| 562 | } | ||
| 563 | |||
| 564 | /// The bytes a request carries, itself or in its uploads. | ||
| 565 | fn carried(&self, peer: &[u8; 16], request: &Request) -> Result<Vec<u8>> { | ||
| 566 | match (&request.bytes, request.handle) { | ||
| 567 | (Some(bytes), _) => Ok(bytes.clone()), | ||
| 568 | (None, Some(handle)) => self | ||
| 569 | .puts | ||
| 570 | .lock() | ||
| 571 | .unwrap() | ||
| 572 | .remove(&(*peer, handle)) | ||
| 573 | .ok_or_else(|| refused(io::ErrorKind::InvalidInput, "No such upload")), | ||
| 574 | (None, None) => Ok(Vec::new()), | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | /// A chunk of a file as it stood when its read began. | ||
| 579 | fn read(&self, peer: &[u8; 16], kind: u16, request: &Request) -> Result<Reply> { | ||
| 580 | let offset = request.offset.unwrap_or_default() as usize; | ||
| 581 | let mut snapshots = self.snapshots.lock().unwrap(); | ||
| 582 | snapshots.retain(|_, (_, read)| read.elapsed() < SNAPSHOT_AGE); | ||
| 583 | let (image, handle) = match request.handle { | ||
| 584 | Some(handle) => { | ||
| 585 | let (image, read) = snapshots | ||
| 586 | .get_mut(&(*peer, handle)) | ||
| 587 | .ok_or_else(|| refused(io::ErrorKind::TimedOut, "The read went stale"))?; | ||
| 588 | *read = Instant::now(); | ||
| 589 | (Arc::clone(image), handle) | ||
| 590 | } | ||
| 591 | None => { | ||
| 592 | drop(snapshots); | ||
| 593 | let limit = (request.limit.unwrap_or(LIMIT as u64) as usize).min(LIMIT); | ||
| 594 | let image = match kind { | ||
| 595 | kind::READ => self.image(&request.path)?, | ||
| 596 | _ => Arc::new(self.storage.read_file(&request.path, limit)?), | ||
| 597 | }; | ||
| 598 | if image.len() > limit { | ||
| 599 | return Err(io::Error::from(io::ErrorKind::FileTooLarge).into()); | ||
| 600 | } | ||
| 601 | snapshots = self.snapshots.lock().unwrap(); | ||
| 602 | if snapshots.keys().filter(|(guest, _)| guest == peer).count() >= SNAPSHOTS { | ||
| 603 | snapshots.retain(|(guest, _), _| guest != peer); | ||
| 604 | } | ||
| 605 | (image, request.id) | ||
| 606 | } | ||
| 607 | }; | ||
| 608 | let end = image.len().min(offset.saturating_add(CHUNK)); | ||
| 609 | let bytes = image.get(offset..end).unwrap_or_default().to_vec(); | ||
| 610 | if end < image.len() { | ||
| 611 | snapshots.insert((*peer, handle), (Arc::clone(&image), Instant::now())); | ||
| 612 | } else { | ||
| 613 | snapshots.remove(&(*peer, handle)); | ||
| 614 | } | ||
| 615 | Ok(Reply { | ||
| 616 | bytes: Some(bytes), | ||
| 617 | length: Some(image.len() as u64), | ||
| 618 | handle: Some(handle), | ||
| 619 | stamp: (kind == kind::READ) | ||
| 620 | .then(|| Stamp::of(&image).ok()) | ||
| 621 | .flatten() | ||
| 622 | .map(|stamp| (&stamp).into()), | ||
| 623 | ..Reply::default() | ||
| 624 | }) | ||
| 625 | } | ||
| 626 | |||
| 627 | /// The section or TOC at `path` as it stands: the image kept for it while its stamp holds. | ||
| 628 | fn image(&self, path: &str) -> Result<Arc<Vec<u8>>> { | ||
| 629 | let stamp = self.storage.stamp(path)?; | ||
| 630 | let kept = self | ||
| 631 | .images | ||
| 632 | .lock() | ||
| 633 | .unwrap() | ||
| 634 | .iter() | ||
| 635 | .find_map(|(held, at, image)| { | ||
| 636 | (held == path && *at == stamp).then(|| Arc::clone(image)) | ||
| 637 | }); | ||
| 638 | if let Some(image) = kept { | ||
| 639 | return Ok(image); | ||
| 640 | } | ||
| 641 | let image = Arc::new(self.storage.read(path)?); | ||
| 642 | self.keep(path, Arc::clone(&image)); | ||
| 643 | Ok(image) | ||
| 644 | } | ||
| 645 | |||
| 646 | fn keep(&self, path: &str, image: Arc<Vec<u8>>) { | ||
| 647 | let Ok(stamp) = Stamp::of(&image) else { | ||
| 648 | return; | ||
| 649 | }; | ||
| 650 | let mut images = self.images.lock().unwrap(); | ||
| 651 | images.retain(|(held, ..)| held != path); | ||
| 652 | images.insert(0, (path.to_owned(), stamp, image)); | ||
| 653 | images.truncate(IMAGES); | ||
| 654 | } | ||
| 655 | |||
| 656 | /// Commits a guest's transaction once the section it makes parses. | ||
| 657 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()> { | ||
| 658 | let not_committed = |error: io::Error| { | ||
| 659 | Error::Remote(CommitError { | ||
| 660 | state: CommitState::NotCommitted, | ||
| 661 | error, | ||
| 662 | }) | ||
| 663 | }; | ||
| 664 | let image = self.image(path).map_err(|error| match error { | ||
| 665 | Error::Io(error) => not_committed(error), | ||
| 666 | error => error, | ||
| 667 | })?; | ||
| 668 | if Stamp::of(&image).ok().as_ref() != Some(transaction.base()) { | ||
| 669 | return Err(not_committed(io::Error::new( | ||
| 670 | io::ErrorKind::ResourceBusy, | ||
| 671 | "The section changed since", | ||
| 672 | ))); | ||
| 673 | } | ||
| 674 | let mut next = (*image).clone(); | ||
| 675 | let checked = transaction.apply(&mut next).and_then(|()| { | ||
| 676 | let store = Store::parse(&next)?; | ||
| 677 | RevisionIndex::parse(&store).map(drop) | ||
| 678 | }); | ||
| 679 | if let Err(error) = checked { | ||
| 680 | return Err(not_committed(io::Error::new( | ||
| 681 | io::ErrorKind::InvalidData, | ||
| 682 | error.to_string(), | ||
| 683 | ))); | ||
| 684 | } | ||
| 685 | self.storage.commit(path, transaction)?; | ||
| 686 | self.keep(path, Arc::new(next)); | ||
| 687 | Ok(()) | ||
| 688 | } | ||
| 689 | } | ||
| 690 | |||
| 691 | fn within(folder: &str, name: &str) -> String { | ||
| 692 | if folder.is_empty() { | ||
| 693 | name.to_owned() | ||
| 694 | } else { | ||
| 695 | format!("{folder}/{name}") | ||
| 696 | } | ||
| 697 | } | ||
| 698 | |||
| 699 | fn wire_entry(entry: &discover::Entry) -> WireEntry { | ||
| 700 | WireEntry { | ||
| 701 | name: entry.name.clone(), | ||
| 702 | kind: match entry.kind { | ||
| 703 | discover::EntryKind::File => 0, | ||
| 704 | discover::EntryKind::Directory => 1, | ||
| 705 | discover::EntryKind::Other => 2, | ||
| 706 | discover::EntryKind::Evicted => 3, | ||
| 707 | }, | ||
| 708 | size: entry.listed.size, | ||
| 709 | modified: entry.listed.modified, | ||
| 710 | } | ||
| 711 | } | ||
| 712 | |||
| 713 | fn entry(entry: &WireEntry) -> discover::Entry { | ||
| 714 | discover::Entry { | ||
| 715 | name: entry.name.clone(), | ||
| 716 | kind: match entry.kind { | ||
| 717 | 0 => discover::EntryKind::File, | ||
| 718 | 1 => discover::EntryKind::Directory, | ||
| 719 | 3 => discover::EntryKind::Evicted, | ||
| 720 | _ => discover::EntryKind::Other, | ||
| 721 | }, | ||
| 722 | listed: discover::Listed { | ||
| 723 | size: entry.size, | ||
| 724 | modified: entry.modified, | ||
| 725 | }, | ||
| 726 | } | ||
| 727 | } | ||
| 728 | |||
| 729 | fn state_number(state: CommitState) -> u8 { | ||
| 730 | match state { | ||
| 731 | CommitState::NotCommitted => 0, | ||
| 732 | CommitState::Unknown => 1, | ||
| 733 | CommitState::Committed => 2, | ||
| 734 | } | ||
| 735 | } | ||
| 736 | |||
| 737 | fn failed(id: u64, error: Error) -> Reply { | ||
| 738 | let (kind, state) = match &error { | ||
| 739 | Error::Io(error) | Error::RemoteIo(error) => (error.kind(), None), | ||
| 740 | Error::Remote(error) => (error.error.kind(), Some(state_number(error.state))), | ||
| 741 | Error::Document(_) => (io::ErrorKind::InvalidData, None), | ||
| 742 | _ => (io::ErrorKind::Other, None), | ||
| 743 | }; | ||
| 744 | let message = match &error { | ||
| 745 | Error::Remote(error) => error.error.to_string(), | ||
| 746 | error => error.to_string(), | ||
| 747 | }; | ||
| 748 | Reply { | ||
| 749 | id, | ||
| 750 | failure: Some(Failure { | ||
| 751 | kind: wire::error_number(kind), | ||
| 752 | message, | ||
| 753 | state, | ||
| 754 | }), | ||
| 755 | ..Reply::default() | ||
| 756 | } | ||
| 757 | } | ||
| 758 | |||
| 759 | /// How a request failed. | ||
| 760 | enum Failed { | ||
| 761 | /// It never left this end. | ||
| 762 | Unsent(io::Error), | ||
| 763 | /// Its answer was lost: whatever it asked may have happened. | ||
| 764 | Lost(io::Error), | ||
| 765 | /// The host refused it. | ||
| 766 | Refused(Failure), | ||
| 767 | } | ||
| 768 | |||
| 769 | impl Failed { | ||
| 770 | fn io(self) -> io::Error { | ||
| 771 | match self { | ||
| 772 | Failed::Unsent(error) | Failed::Lost(error) => error, | ||
| 773 | Failed::Refused(failure) => { | ||
| 774 | io::Error::new(wire::error_kind(failure.kind), failure.message) | ||
| 775 | } | ||
| 776 | } | ||
| 777 | } | ||
| 778 | |||
| 779 | /// As a commit's failure: one never sent was not committed, one whose answer was lost may | ||
| 780 | /// have been. | ||
| 781 | fn commit(self) -> CommitError { | ||
| 782 | let state = match &self { | ||
| 783 | Failed::Unsent(_) => CommitState::NotCommitted, | ||
| 784 | Failed::Lost(_) => CommitState::Unknown, | ||
| 785 | Failed::Refused(failure) => match failure.state { | ||
| 786 | Some(1) => CommitState::Unknown, | ||
| 787 | Some(2) => CommitState::Committed, | ||
| 788 | _ => CommitState::NotCommitted, | ||
| 789 | }, | ||
| 790 | }; | ||
| 791 | CommitError { | ||
| 792 | state, | ||
| 793 | error: self.io(), | ||
| 794 | } | ||
| 795 | } | ||
| 796 | } | ||
| 797 | |||
| 798 | /// A guest's way to a share: the share's room, and the requests waiting on its host. | ||
| 799 | pub struct Guest { | ||
| 800 | live: Live, | ||
| 801 | inner: Arc<Inner>, | ||
| 802 | } | ||
| 803 | |||
| 804 | struct Inner { | ||
| 805 | share: [u8; 16], | ||
| 806 | /// The host and the line to it, while connected. | ||
| 807 | host: Mutex<Option<(Arc<Hello>, Line)>>, | ||
| 808 | pending: Mutex<HashMap<u64, mpsc::Sender<Reply>>>, | ||
| 809 | next: AtomicU64, | ||
| 810 | /// Where the host's reports of changed files go, while a background watches. | ||
| 811 | watch: Mutex<Option<Reports>>, | ||
| 812 | /// The host stopped sharing. | ||
| 813 | stopped: AtomicBool, | ||
| 814 | /// Bytes of chunks asked for and not yet given. | ||
| 815 | asked: (Mutex<usize>, Condvar), | ||
| 816 | } | ||
| 817 | |||
| 818 | impl Guest { | ||
| 819 | /// Joins share `share` through its `secret` as `me`, where `reach` and `relay` say. | ||
| 820 | /// `events` runs on a network thread whenever the host comes or goes, or the peers change. | ||
| 821 | pub fn start( | ||
| 822 | me: Hello, | ||
| 823 | share: [u8; 16], | ||
| 824 | secret: [u8; 16], | ||
| 825 | reach: Option<Reach>, | ||
| 826 | relay: Option<&str>, | ||
| 827 | events: impl Fn() + Send + Sync + 'static, | ||
| 828 | ) -> io::Result<Arc<Self>> { | ||
| 829 | let inner = Arc::new(Inner { | ||
| 830 | share, | ||
| 831 | host: Mutex::default(), | ||
| 832 | pending: Mutex::default(), | ||
| 833 | next: AtomicU64::new(1), | ||
| 834 | watch: Mutex::default(), | ||
| 835 | stopped: AtomicBool::new(false), | ||
| 836 | asked: Default::default(), | ||
| 837 | }); | ||
| 838 | let heard = Arc::clone(&inner); | ||
| 839 | let live = Live::start(me, &Room::Notebook(secret), reach, relay, move |event| { | ||
| 840 | heard.heard(event); | ||
| 841 | events(); | ||
| 842 | })?; | ||
| 843 | Ok(Arc::new(Self { live, inner })) | ||
| 844 | } | ||
| 845 | |||
| 846 | /// Where the notebook is kept: `location(share)`. | ||
| 847 | pub fn location(&self) -> String { | ||
| 848 | location(&self.inner.share) | ||
| 849 | } | ||
| 850 | |||
| 851 | /// The host, while connected. | ||
| 852 | pub fn host(&self) -> Option<Arc<Hello>> { | ||
| 853 | let host = self.inner.host.lock().unwrap(); | ||
| 854 | host.as_ref().map(|(hello, _)| Arc::clone(hello)) | ||
| 855 | } | ||
| 856 | |||
| 857 | /// Whether the host said it stopped sharing. | ||
| 858 | pub fn stopped(&self) -> bool { | ||
| 859 | self.inner.stopped.load(Ordering::Acquire) | ||
| 860 | } | ||
| 861 | |||
| 862 | /// Everyone in the share's room: the host and the other guests. | ||
| 863 | pub fn peers(&self) -> Vec<Peer> { | ||
| 864 | self.live.peers() | ||
| 865 | } | ||
| 866 | |||
| 867 | pub fn set_presence(&self, presence: Presence) { | ||
| 868 | self.live.set_presence(presence); | ||
| 869 | } | ||
| 870 | |||
| 871 | /// Sends the host's reports of changed files to `reports` while it stays connected. | ||
| 872 | pub(crate) fn watch(&self, reports: Reports) -> io::Result<()> { | ||
| 873 | if self.host().is_none() { | ||
| 874 | return Err(self.offline()); | ||
| 875 | } | ||
| 876 | *self.inner.watch.lock().unwrap() = Some(reports); | ||
| 877 | Ok(()) | ||
| 878 | } | ||
| 879 | |||
| 880 | fn offline(&self) -> io::Error { | ||
| 881 | let message = if self.stopped() { | ||
| 882 | "The host stopped sharing this notebook" | ||
| 883 | } else { | ||
| 884 | "The computer sharing this notebook can’t be reached" | ||
| 885 | }; | ||
| 886 | io::Error::new(io::ErrorKind::NotConnected, message) | ||
| 887 | } | ||
| 888 | |||
| 889 | /// Asks the host `kind` of `request`, waiting for its reply. | ||
| 890 | fn request(&self, kind: u16, mut request: Request) -> std::result::Result<Reply, Failed> { | ||
| 891 | let Some((_, line)) = self.inner.host.lock().unwrap().clone() else { | ||
| 892 | return Err(Failed::Unsent(self.offline())); | ||
| 893 | }; | ||
| 894 | let chunked = matches!(kind, kind::READ | kind::READ_FILE | kind::PUT); | ||
| 895 | if chunked { | ||
| 896 | let (asked, room) = &self.inner.asked; | ||
| 897 | let mut asked = asked.lock().unwrap(); | ||
| 898 | while *asked + CHUNK > WINDOW { | ||
| 899 | asked = room.wait(asked).unwrap(); | ||
| 900 | } | ||
| 901 | *asked += CHUNK; | ||
| 902 | } | ||
| 903 | let id = self.inner.next.fetch_add(1, Ordering::Relaxed); | ||
| 904 | request.id = id; | ||
| 905 | let (answer, answered) = mpsc::channel(); | ||
| 906 | self.inner.pending.lock().unwrap().insert(id, answer); | ||
| 907 | let sent = line.send(kind, &request); | ||
| 908 | let reply = match sent { | ||
| 909 | Err(error) => Err(Failed::Unsent(error)), | ||
| 910 | Ok(()) => match answered.recv_timeout(TIMEOUT) { | ||
| 911 | Ok(reply) => Ok(reply), | ||
| 912 | Err(mpsc::RecvTimeoutError::Timeout) => Err(Failed::Lost(io::Error::new( | ||
| 913 | io::ErrorKind::TimedOut, | ||
| 914 | "The computer sharing this notebook didn’t answer", | ||
| 915 | ))), | ||
| 916 | Err(mpsc::RecvTimeoutError::Disconnected) => Err(Failed::Lost(self.offline())), | ||
| 917 | }, | ||
| 918 | }; | ||
| 919 | self.inner.pending.lock().unwrap().remove(&id); | ||
| 920 | if chunked { | ||
| 921 | let (asked, room) = &self.inner.asked; | ||
| 922 | *asked.lock().unwrap() -= CHUNK; | ||
| 923 | room.notify_all(); | ||
| 924 | } | ||
| 925 | let reply = reply?; | ||
| 926 | match reply.failure { | ||
| 927 | Some(failure) => Err(Failed::Refused(failure)), | ||
| 928 | None => Ok(reply), | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | fn ask(&self, kind: u16, request: Request) -> io::Result<Reply> { | ||
| 933 | self.request(kind, request).map_err(Failed::io) | ||
| 934 | } | ||
| 935 | |||
| 936 | /// Puts `bytes` in `request`, or uploads them first where they are large. | ||
| 937 | fn carry(&self, request: &mut Request, bytes: Vec<u8>) -> std::result::Result<(), Failed> { | ||
| 938 | if bytes.len() <= CHUNK { | ||
| 939 | request.bytes = Some(bytes); | ||
| 940 | return Ok(()); | ||
| 941 | } | ||
| 942 | let handle = self.inner.next.fetch_add(1, Ordering::Relaxed); | ||
| 943 | for (index, chunk) in bytes.chunks(CHUNK).enumerate() { | ||
| 944 | let put = Request { | ||
| 945 | handle: Some(handle), | ||
| 946 | offset: Some((index * CHUNK) as u64), | ||
| 947 | bytes: Some(chunk.to_vec()), | ||
| 948 | ..Request::default() | ||
| 949 | }; | ||
| 950 | // Nothing the upload carries happens before the request that uses it. | ||
| 951 | self.request(kind::PUT, put) | ||
| 952 | .map_err(|failed| match failed { | ||
| 953 | Failed::Lost(error) => Failed::Unsent(error), | ||
| 954 | failed => failed, | ||
| 955 | })?; | ||
| 956 | } | ||
| 957 | request.handle = Some(handle); | ||
| 958 | Ok(()) | ||
| 959 | } | ||
| 960 | |||
| 961 | /// Reads the file at `path` a chunk at a time, as `kind` reads it. | ||
| 962 | fn read(&self, kind: u16, path: &str, limit: usize) -> io::Result<Vec<u8>> { | ||
| 963 | let first = self.ask( | ||
| 964 | kind, | ||
| 965 | Request { | ||
| 966 | path: path.to_owned(), | ||
| 967 | offset: Some(0), | ||
| 968 | limit: Some(limit as u64), | ||
| 969 | ..Request::default() | ||
| 970 | }, | ||
| 971 | )?; | ||
| 972 | let length = first.length.unwrap_or_default() as usize; | ||
| 973 | if length > limit { | ||
| 974 | return Err(io::ErrorKind::FileTooLarge.into()); | ||
| 975 | } | ||
| 976 | let mut image = first.bytes.unwrap_or_default(); | ||
| 977 | while image.len() < length { | ||
| 978 | let chunk = self | ||
| 979 | .ask( | ||
| 980 | kind, | ||
| 981 | Request { | ||
| 982 | path: path.to_owned(), | ||
| 983 | offset: Some(image.len() as u64), | ||
| 984 | handle: first.handle, | ||
| 985 | ..Request::default() | ||
| 986 | }, | ||
| 987 | )? | ||
| 988 | .bytes | ||
| 989 | .unwrap_or_default(); | ||
| 990 | if chunk.is_empty() { | ||
| 991 | return Err(io::ErrorKind::UnexpectedEof.into()); | ||
| 992 | } | ||
| 993 | image.extend_from_slice(&chunk); | ||
| 994 | } | ||
| 995 | image.truncate(length); | ||
| 996 | Ok(image) | ||
| 997 | } | ||
| 998 | |||
| 999 | pub(crate) fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 1000 | let reply = self.ask( | ||
| 1001 | kind::LIST, | ||
| 1002 | Request { | ||
| 1003 | path: folder.to_owned(), | ||
| 1004 | ..Request::default() | ||
| 1005 | }, | ||
| 1006 | )?; | ||
| 1007 | Ok(reply | ||
| 1008 | .entries | ||
| 1009 | .unwrap_or_default() | ||
| 1010 | .iter() | ||
| 1011 | .map(entry) | ||
| 1012 | .collect()) | ||
| 1013 | } | ||
| 1014 | |||
| 1015 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | ||
| 1016 | let reply = self.ask( | ||
| 1017 | kind::STAMP, | ||
| 1018 | Request { | ||
| 1019 | path: path.to_owned(), | ||
| 1020 | ..Request::default() | ||
| 1021 | }, | ||
| 1022 | )?; | ||
| 1023 | reply | ||
| 1024 | .stamp | ||
| 1025 | .as_ref() | ||
| 1026 | .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))? | ||
| 1027 | .try_into() | ||
| 1028 | } | ||
| 1029 | |||
| 1030 | fn commit( | ||
| 1031 | &self, | ||
| 1032 | path: &str, | ||
| 1033 | transaction: &Transaction, | ||
| 1034 | ) -> std::result::Result<(), CommitError> { | ||
| 1035 | let mut request = Request { | ||
| 1036 | path: path.to_owned(), | ||
| 1037 | ..Request::default() | ||
| 1038 | }; | ||
| 1039 | self.carry(&mut request, transaction.to_bytes()) | ||
| 1040 | .map_err(Failed::commit)?; | ||
| 1041 | self.request(kind::COMMIT, request) | ||
| 1042 | .map(drop) | ||
| 1043 | .map_err(Failed::commit) | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 1047 | let request = Request { | ||
| 1048 | path: path.to_owned(), | ||
| 1049 | stamp: Some(base.into()), | ||
| 1050 | ..Request::default() | ||
| 1051 | }; | ||
| 1052 | self.request(kind::CONFIRM, request) | ||
| 1053 | .map(drop) | ||
| 1054 | .map_err(Failed::commit) | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | /// A request on `path` that answers nothing but whether it happened. | ||
| 1058 | fn verb(&self, kind: u16, path: &str, request: Request) -> Result<()> { | ||
| 1059 | self.ask( | ||
| 1060 | kind, | ||
| 1061 | Request { | ||
| 1062 | path: path.to_owned(), | ||
| 1063 | ..request | ||
| 1064 | }, | ||
| 1065 | )?; | ||
| 1066 | Ok(()) | ||
| 1067 | } | ||
| 1068 | } | ||
| 1069 | |||
| 1070 | impl Inner { | ||
| 1071 | fn heard(&self, event: Event) { | ||
| 1072 | let serves = |hello: &Hello| hello.serves == Some(self.share); | ||
| 1073 | match event { | ||
| 1074 | Event::Met(hello, line) if serves(hello) => { | ||
| 1075 | *self.host.lock().unwrap() = Some((Arc::clone(hello), line.clone())); | ||
| 1076 | } | ||
| 1077 | Event::Left(hello) if serves(hello) => { | ||
| 1078 | *self.host.lock().unwrap() = None; | ||
| 1079 | // Each request waiting hears its answer was lost. | ||
| 1080 | self.pending.lock().unwrap().clear(); | ||
| 1081 | if let Some(reports) = self.watch.lock().unwrap().take() { | ||
| 1082 | reports.lost(); | ||
| 1083 | } | ||
| 1084 | } | ||
| 1085 | Event::Frame { | ||
| 1086 | from, kind, body, .. | ||
| 1087 | } => match kind { | ||
| 1088 | kind::REPLY if serves(from) => { | ||
| 1089 | if let Ok(reply) = minicbor::decode::<Reply>(body) | ||
| 1090 | && let Some(waiting) = self.pending.lock().unwrap().remove(&reply.id) | ||
| 1091 | { | ||
| 1092 | let _ = waiting.send(reply); | ||
| 1093 | } | ||
| 1094 | } | ||
| 1095 | kind::TOUCHED if serves(from) => { | ||
| 1096 | if let Ok(touched) = minicbor::decode::<Touched>(body) | ||
| 1097 | && let Some(reports) = &*self.watch.lock().unwrap() | ||
| 1098 | { | ||
| 1099 | reports.touched(&touched.paths); | ||
| 1100 | } | ||
| 1101 | } | ||
| 1102 | kind::BYE | ||
| 1103 | if serves(from) | ||
| 1104 | && minicbor::decode::<wire::Bye>(body) | ||
| 1105 | .is_ok_and(|bye| bye.reason == STOPPED) => | ||
| 1106 | { | ||
| 1107 | self.stopped.store(true, Ordering::Release); | ||
| 1108 | } | ||
| 1109 | _ => {} | ||
| 1110 | }, | ||
| 1111 | _ => {} | ||
| 1112 | } | ||
| 1113 | } | ||
| 1114 | } | ||
| 1115 | |||
| 1116 | /// A section a Live Share host serves, as a guest's replica publishes to it. | ||
| 1117 | pub struct HostedRemote { | ||
| 1118 | guest: Arc<Guest>, | ||
| 1119 | path: String, | ||
| 1120 | } | ||
| 1121 | |||
| 1122 | impl HostedRemote { | ||
| 1123 | pub fn new(guest: &Arc<Guest>, path: &str) -> Self { | ||
| 1124 | Self { | ||
| 1125 | guest: Arc::clone(guest), | ||
| 1126 | path: path.to_owned(), | ||
| 1127 | } | ||
| 1128 | } | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | impl crate::Remote for HostedRemote { | ||
| 1132 | fn read(&mut self) -> io::Result<Vec<u8>> { | ||
| 1133 | self.guest.read(kind::READ, &self.path, LIMIT) | ||
| 1134 | } | ||
| 1135 | |||
| 1136 | fn stamp(&mut self) -> io::Result<Stamp> { | ||
| 1137 | self.guest.stamp(&self.path) | ||
| 1138 | } | ||
| 1139 | |||
| 1140 | fn publish(&mut self, transaction: &Transaction) -> std::result::Result<(), CommitError> { | ||
| 1141 | self.guest.commit(&self.path, transaction) | ||
| 1142 | } | ||
| 1143 | |||
| 1144 | fn confirm(&mut self, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 1145 | self.guest.confirm(&self.path, base) | ||
| 1146 | } | ||
| 1147 | } | ||
| 1148 | |||
| 1149 | /// A notebook a Live Share host serves, as a guest's `session::Notebook` reaches it. Its | ||
| 1150 | /// folders as last listed are kept at `listed`, so the notebook opens while the host can't | ||
| 1151 | /// be reached. | ||
| 1152 | pub struct Hosted { | ||
| 1153 | guest: Arc<Guest>, | ||
| 1154 | listed: PathBuf, | ||
| 1155 | } | ||
| 1156 | |||
| 1157 | /// Each folder's entries as last listed: name, `WireEntry::kind`, size and modified time. | ||
| 1158 | type Listings = BTreeMap<String, Vec<(String, u8, u64, u64)>>; | ||
| 1159 | |||
| 1160 | impl Hosted { | ||
| 1161 | pub(crate) fn new(guest: Arc<Guest>, listed: PathBuf) -> Self { | ||
| 1162 | Self { guest, listed } | ||
| 1163 | } | ||
| 1164 | } | ||
| 1165 | |||
| 1166 | /// The host's folders, or as they were last listed while it can't be reached. | ||
| 1167 | struct Source<'a> { | ||
| 1168 | guest: &'a Guest, | ||
| 1169 | kept: Listings, | ||
| 1170 | listed: Listings, | ||
| 1171 | } | ||
| 1172 | |||
| 1173 | impl discover::Source for Source<'_> { | ||
| 1174 | fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<discover::Entry>> { | ||
| 1175 | let entries = match self.guest.entries(path) { | ||
| 1176 | Ok(entries) => entries, | ||
| 1177 | Err(error) if error.kind() == io::ErrorKind::NotConnected => self | ||
| 1178 | .kept | ||
| 1179 | .get(path) | ||
| 1180 | .ok_or(error)? | ||
| 1181 | .iter() | ||
| 1182 | .map(|(name, kind, size, modified)| { | ||
| 1183 | entry(&WireEntry { | ||
| 1184 | name: name.clone(), | ||
| 1185 | kind: *kind, | ||
| 1186 | size: *size, | ||
| 1187 | modified: *modified, | ||
| 1188 | }) | ||
| 1189 | }) | ||
| 1190 | .collect(), | ||
| 1191 | Err(error) => return Err(error), | ||
| 1192 | }; | ||
| 1193 | if entries.len() > limit { | ||
| 1194 | return Err(io::ErrorKind::FileTooLarge.into()); | ||
| 1195 | } | ||
| 1196 | self.listed.insert( | ||
| 1197 | path.to_owned(), | ||
| 1198 | entries | ||
| 1199 | .iter() | ||
| 1200 | .map(|listed| { | ||
| 1201 | let wire = wire_entry(listed); | ||
| 1202 | (wire.name, wire.kind, wire.size, wire.modified) | ||
| 1203 | }) | ||
| 1204 | .collect(), | ||
| 1205 | ); | ||
| 1206 | Ok(entries) | ||
| 1207 | } | ||
| 1208 | |||
| 1209 | fn read(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { | ||
| 1210 | self.guest.read(kind::READ, path, limit) | ||
| 1211 | } | ||
| 1212 | |||
| 1213 | fn read_asset(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> { | ||
| 1214 | self.guest.read(kind::READ_FILE, path, limit) | ||
| 1215 | } | ||
| 1216 | } | ||
| 1217 | |||
| 1218 | impl Storage for Hosted { | ||
| 1219 | fn discover( | ||
| 1220 | &self, | ||
| 1221 | cache: &mut discover::Cache, | ||
| 1222 | limits: discover::Limits, | ||
| 1223 | ) -> Result<discover::Folder> { | ||
| 1224 | let kept = crate::fs::read(&self.listed) | ||
| 1225 | .ok() | ||
| 1226 | .and_then(|bytes| serde_json::from_slice(&bytes).ok()) | ||
| 1227 | .unwrap_or_default(); | ||
| 1228 | let mut source = Source { | ||
| 1229 | guest: &self.guest, | ||
| 1230 | kept, | ||
| 1231 | listed: Listings::new(), | ||
| 1232 | }; | ||
| 1233 | let folder = cache.discover(&mut source, limits)?; | ||
| 1234 | if let Ok(json) = serde_json::to_vec(&source.listed) { | ||
| 1235 | let _ = crate::fs::write(&self.listed, json); | ||
| 1236 | } | ||
| 1237 | Ok(folder) | ||
| 1238 | } | ||
| 1239 | |||
| 1240 | fn location(&self) -> String { | ||
| 1241 | self.guest.location() | ||
| 1242 | } | ||
| 1243 | |||
| 1244 | fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 1245 | self.guest.entries(folder) | ||
| 1246 | } | ||
| 1247 | |||
| 1248 | fn exists(&self, path: &str) -> bool { | ||
| 1249 | let request = Request { | ||
| 1250 | path: path.to_owned(), | ||
| 1251 | ..Request::default() | ||
| 1252 | }; | ||
| 1253 | self.guest | ||
| 1254 | .ask(kind::EXISTS, request) | ||
| 1255 | .is_ok_and(|reply| reply.exists == Some(true)) | ||
| 1256 | } | ||
| 1257 | |||
| 1258 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | ||
| 1259 | self.guest.stamp(path) | ||
| 1260 | } | ||
| 1261 | |||
| 1262 | fn read(&self, path: &str) -> Result<Vec<u8>> { | ||
| 1263 | Ok(self.guest.read(kind::READ, path, LIMIT)?) | ||
| 1264 | } | ||
| 1265 | |||
| 1266 | fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>> { | ||
| 1267 | Ok(self.guest.read(kind::READ_FILE, path, limit)?) | ||
| 1268 | } | ||
| 1269 | |||
| 1270 | fn create(&self, path: &str, bytes: &[u8]) -> Result<()> { | ||
| 1271 | let mut request = Request { | ||
| 1272 | path: path.to_owned(), | ||
| 1273 | ..Request::default() | ||
| 1274 | }; | ||
| 1275 | self.guest | ||
| 1276 | .carry(&mut request, bytes.to_vec()) | ||
| 1277 | .map_err(Failed::io)?; | ||
| 1278 | self.guest.verb(kind::CREATE, path, request) | ||
| 1279 | } | ||
| 1280 | |||
| 1281 | fn create_directory(&self, path: &str) -> Result<()> { | ||
| 1282 | self.guest | ||
| 1283 | .verb(kind::CREATE_DIRECTORY, path, Request::default()) | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | fn hide(&self, path: &str) -> Result<()> { | ||
| 1287 | self.guest.verb(kind::HIDE, path, Request::default()) | ||
| 1288 | } | ||
| 1289 | |||
| 1290 | fn rename(&self, from: &str, to: &str) -> Result<()> { | ||
| 1291 | let request = Request { | ||
| 1292 | to: Some(to.to_owned()), | ||
| 1293 | ..Request::default() | ||
| 1294 | }; | ||
| 1295 | self.guest.verb(kind::RENAME, from, request) | ||
| 1296 | } | ||
| 1297 | |||
| 1298 | fn rename_root(&self, _: &str, _: &[String]) -> Result<String> { | ||
| 1299 | Err(refused( | ||
| 1300 | io::ErrorKind::Unsupported, | ||
| 1301 | "Only the computer sharing this notebook can rename its folder", | ||
| 1302 | )) | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | fn replace(&self, from: &str, to: &str) -> Result<()> { | ||
| 1306 | let request = Request { | ||
| 1307 | to: Some(to.to_owned()), | ||
| 1308 | ..Request::default() | ||
| 1309 | }; | ||
| 1310 | self.guest.verb(kind::REPLACE, from, request) | ||
| 1311 | } | ||
| 1312 | |||
| 1313 | fn delete(&self, path: &str) -> Result<()> { | ||
| 1314 | self.guest.verb(kind::DELETE, path, Request::default()) | ||
| 1315 | } | ||
| 1316 | |||
| 1317 | fn place(&self, path: &str, ancestor: [u8; 16], name: &str) -> Result<()> { | ||
| 1318 | let request = Request { | ||
| 1319 | ancestor: Some(ancestor), | ||
| 1320 | name: Some(name.to_owned()), | ||
| 1321 | ..Request::default() | ||
| 1322 | }; | ||
| 1323 | self.guest.verb(kind::PLACE, path, request) | ||
| 1324 | } | ||
| 1325 | |||
| 1326 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()> { | ||
| 1327 | Ok(self.guest.commit(path, transaction)?) | ||
| 1328 | } | ||
| 1329 | |||
| 1330 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 1331 | self.guest.confirm(path, base) | ||
| 1332 | } | ||
| 1333 | |||
| 1334 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { | ||
| 1335 | let request = Request { | ||
| 1336 | to: Some(with.to_owned()), | ||
| 1337 | stamp: Some(WireStamp::from(base)), | ||
| 1338 | ..Request::default() | ||
| 1339 | }; | ||
| 1340 | self.guest.verb(kind::SUPERSEDE, path, request) | ||
| 1341 | } | ||
| 1342 | } | ||
crates/notebook/src/live/tests.rs+35-25| ... | @@ -43,9 +43,9 @@ fn caret(offset: u32) -> Presence { | ... | @@ -43,9 +43,9 @@ fn caret(offset: u32) -> Presence { |
| 43 | /// caret, and see the other leave. | 43 | /// caret, and see the other leave. |
| 44 | #[test] | 44 | #[test] |
| 45 | fn peers_meet_and_follow_presence() { | 45 | fn peers_meet_and_follow_presence() { |
| 46 | let room = Room::Code("7-violet-otter".into()); | 46 | let room = Room::join("7-violet-otter", ""); |
| 47 | let ada = Live::start(hello("Ada"), &room, None, None, || {}).unwrap(); | 47 | let ada = Live::start(hello("Ada"), &room, None, None, |_| {}).unwrap(); |
| 48 | let grace = Live::start(hello("Grace"), &room, None, None, || {}).unwrap(); | 48 | let grace = Live::start(hello("Grace"), &room, None, None, |_| {}).unwrap(); |
| 49 | ada.set_presence(caret(1)); | 49 | ada.set_presence(caret(1)); |
| 50 | ada.connect(grace.address()); | 50 | ada.connect(grace.address()); |
| 51 | let seen = until(&grace, |peers| { | 51 | let seen = until(&grace, |peers| { |
| ... | @@ -70,18 +70,18 @@ fn peers_meet_and_follow_presence() { | ... | @@ -70,18 +70,18 @@ fn peers_meet_and_follow_presence() { |
| 70 | fn another_code_never_meets() { | 70 | fn another_code_never_meets() { |
| 71 | let ada = Live::start( | 71 | let ada = Live::start( |
| 72 | hello("Ada"), | 72 | hello("Ada"), |
| 73 | &Room::Code("7-violet-otter".into()), | 73 | &Room::join("7-violet-otter", ""), |
| 74 | None, | 74 | None, |
| 75 | None, | 75 | None, |
| 76 | || {}, | 76 | |_| {}, |
| 77 | ) | 77 | ) |
| 78 | .unwrap(); | 78 | .unwrap(); |
| 79 | let mallory = Live::start( | 79 | let mallory = Live::start( |
| 80 | hello("Mallory"), | 80 | hello("Mallory"), |
| 81 | &Room::Code("7-violet-ocelot".into()), | 81 | &Room::join("7-violet-ocelot", ""), |
| 82 | None, | 82 | None, |
| 83 | None, | 83 | None, |
| 84 | || {}, | 84 | |_| {}, |
| 85 | ) | 85 | ) |
| 86 | .unwrap(); | 86 | .unwrap(); |
| 87 | mallory.connect(ada.address()); | 87 | mallory.connect(ada.address()); |
| ... | @@ -115,8 +115,8 @@ fn later_fields_and_kinds_are_skipped() { | ... | @@ -115,8 +115,8 @@ fn later_fields_and_kinds_are_skipped() { |
| 115 | } | 115 | } |
| 116 | ); | 116 | ); |
| 117 | 117 | ||
| 118 | let room = Room::Code("4-quiet-heron".into()); | 118 | let room = Room::join("4-quiet-heron", ""); |
| 119 | let grace = Live::start(hello("Grace"), &room, None, None, || {}).unwrap(); | 119 | let grace = Live::start(hello("Grace"), &room, None, None, |_| {}).unwrap(); |
| 120 | // A later version: it greets, says something new, then where it is. | 120 | // A later version: it greets, says something new, then where it is. |
| 121 | let later = thread::spawn(move || { | 121 | let later = thread::spawn(move || { |
| 122 | let mut stream = TcpStream::connect(grace.address()).unwrap(); | 122 | let mut stream = TcpStream::connect(grace.address()).unwrap(); |
| ... | @@ -146,8 +146,8 @@ fn later_fields_and_kinds_are_skipped() { | ... | @@ -146,8 +146,8 @@ fn later_fields_and_kinds_are_skipped() { |
| 146 | #[ignore = "multicasts mDNS on the loopback interface"] | 146 | #[ignore = "multicasts mDNS on the loopback interface"] |
| 147 | fn peers_find_each_other_on_loopback() { | 147 | fn peers_find_each_other_on_loopback() { |
| 148 | let room = Room::Notebook([9; 16]); | 148 | let room = Room::Notebook([9; 16]); |
| 149 | let ada = Live::start(hello("Ada"), &room, Some(Reach::Loopback), None, || {}).unwrap(); | 149 | let ada = Live::start(hello("Ada"), &room, Some(Reach::Loopback), None, |_| {}).unwrap(); |
| 150 | let grace = Live::start(hello("Grace"), &room, Some(Reach::Loopback), None, || {}).unwrap(); | 150 | let grace = Live::start(hello("Grace"), &room, Some(Reach::Loopback), None, |_| {}).unwrap(); |
| 151 | until(&ada, |peers| peers.len() == 1); | 151 | until(&ada, |peers| peers.len() == 1); |
| 152 | until(&grace, |peers| peers.len() == 1); | 152 | until(&grace, |peers| peers.len() == 1); |
| 153 | } | 153 | } |
| ... | @@ -272,9 +272,9 @@ fn status(address: SocketAddr, path: &str) -> String { | ... | @@ -272,9 +272,9 @@ fn status(address: SocketAddr, path: &str) -> String { |
| 272 | fn peers_meet_through_a_relay() { | 272 | fn peers_meet_through_a_relay() { |
| 273 | let (url, _) = relay(Default::default()); | 273 | let (url, _) = relay(Default::default()); |
| 274 | let room = Room::Notebook([3; 16]); | 274 | let room = Room::Notebook([3; 16]); |
| 275 | let ada = Live::start(hello("Ada"), &room, None, Some(&url), || {}).unwrap(); | 275 | let ada = Live::start(hello("Ada"), &room, None, Some(&url), |_| {}).unwrap(); |
| 276 | ada.set_presence(caret(1)); | 276 | ada.set_presence(caret(1)); |
| 277 | let grace = Live::start(hello("Grace"), &room, None, Some(&url), || {}).unwrap(); | 277 | let grace = Live::start(hello("Grace"), &room, None, Some(&url), |_| {}).unwrap(); |
| 278 | until(&grace, |peers| { | 278 | until(&grace, |peers| { |
| 279 | peers.len() == 1 && peers[0].presence == Some(caret(1)) | 279 | peers.len() == 1 && peers[0].presence == Some(caret(1)) |
| 280 | }); | 280 | }); |
| ... | @@ -298,10 +298,10 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() { | ... | @@ -298,10 +298,10 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() { |
| 298 | }); | 298 | }); |
| 299 | let host = Live::start( | 299 | let host = Live::start( |
| 300 | hello("Ada"), | 300 | hello("Ada"), |
| 301 | &Room::Code("violet-otter".into()), | 301 | &Room::share("violet-otter", ""), |
| 302 | None, | 302 | None, |
| 303 | Some(&url), | 303 | Some(&url), |
| 304 | || {}, | 304 | |_| {}, |
| 305 | ) | 305 | ) |
| 306 | .unwrap(); | 306 | .unwrap(); |
| 307 | let deadline = Instant::now() + Duration::from_secs(10); | 307 | let deadline = Instant::now() + Duration::from_secs(10); |
| ... | @@ -316,23 +316,30 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() { | ... | @@ -316,23 +316,30 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() { |
| 316 | assert_eq!(words, "violet-otter"); | 316 | assert_eq!(words, "violet-otter"); |
| 317 | let guest = Live::start( | 317 | let guest = Live::start( |
| 318 | hello("Grace"), | 318 | hello("Grace"), |
| 319 | &Room::Code(code.clone()), | 319 | &Room::join(&code, ""), |
| 320 | None, | 320 | None, |
| 321 | Some(&url), | 321 | Some(&url), |
| 322 | || {}, | 322 | |_| {}, |
| 323 | ) | 323 | ) |
| 324 | .unwrap(); | 324 | .unwrap(); |
| 325 | until(&host, |peers| peers.len() == 1); | 325 | until(&host, |peers| peers.len() == 1); |
| 326 | until(&guest, |peers| peers.len() == 1); | 326 | until(&guest, |peers| peers.len() == 1); |
| 327 | 327 | ||
| 328 | let path = format!("/v1/room/code-{number}"); | 328 | let path = format!("/v1/room/code-{number}"); |
| 329 | let wrong = Room::Code(format!("{number}-violet-ocelot")); | 329 | let wrong = Room::join(&format!("{number}-violet-ocelot"), ""); |
| 330 | let mallory = Live::start(hello("Mallory"), &wrong, None, Some(&url), || {}).unwrap(); | 330 | // An end that typed a wrong code gives up at once; Mallory tries twice, and the second |
| 331 | // Mallory tries again a second later, and the second wrong try burns the code. Asking | 331 | // wrong try burns the code. |
| 332 | // sooner would count as a try itself. | 332 | for _ in 0..2 { |
| 333 | thread::sleep(Duration::from_secs(5)); | 333 | let mallory = Live::start(hello("Mallory"), &wrong, None, Some(&url), |_| {}).unwrap(); |
| 334 | let deadline = Instant::now() + Duration::from_secs(10); | ||
| 335 | while mallory.failed() == 0 { | ||
| 336 | assert!(Instant::now() < deadline, "Mallory never tried"); | ||
| 337 | thread::sleep(Duration::from_millis(20)); | ||
| 338 | } | ||
| 339 | assert!(mallory.peers().is_empty()); | ||
| 340 | } | ||
| 341 | until(&host, |_| host.burned()); | ||
| 334 | assert_eq!(status(address, &path), "HTTP/1.1 410 Gone"); | 342 | assert_eq!(status(address, &path), "HTTP/1.1 410 Gone"); |
| 335 | assert!(mallory.peers().is_empty()); | ||
| 336 | assert_eq!(host.peers().len(), 1, "Grace stays"); | 343 | assert_eq!(host.peers().len(), 1, "Grace stays"); |
| 337 | } | 344 | } |
| 338 | 345 | ||
| ... | @@ -441,7 +448,10 @@ fn recording(name: &str, room: &Room, relay: &str) -> (Live, Arc<Mutex<Vec<Optio | ... | @@ -441,7 +448,10 @@ fn recording(name: &str, room: &Room, relay: &str) -> (Live, Arc<Mutex<Vec<Optio |
| 441 | let heard = Arc::new(Mutex::new(Vec::new())); | 448 | let heard = Arc::new(Mutex::new(Vec::new())); |
| 442 | let shared: Arc<std::sync::OnceLock<std::sync::Weak<Shared>>> = Arc::default(); | 449 | let shared: Arc<std::sync::OnceLock<std::sync::Weak<Shared>>> = Arc::default(); |
| 443 | let (recorded, watched) = (Arc::clone(&heard), Arc::clone(&shared)); | 450 | let (recorded, watched) = (Arc::clone(&heard), Arc::clone(&shared)); |
| 444 | let live = Live::start(hello(name), room, None, Some(relay), move || { | 451 | let live = Live::start(hello(name), room, None, Some(relay), move |event| { |
| 452 | if !matches!(event, Event::Changed) { | ||
| 453 | return; | ||
| 454 | } | ||
| 445 | let Some(shared) = watched.get().and_then(std::sync::Weak::upgrade) else { | 455 | let Some(shared) = watched.get().and_then(std::sync::Weak::upgrade) else { |
| 446 | return; | 456 | return; |
| 447 | }; | 457 | }; |
| ... | @@ -470,7 +480,7 @@ fn a_malicious_relay_is_caught() { | ... | @@ -470,7 +480,7 @@ fn a_malicious_relay_is_caught() { |
| 470 | Tamper::Inject, | 480 | Tamper::Inject, |
| 471 | ] { | 481 | ] { |
| 472 | let (url, address) = relay(Default::default()); | 482 | let (url, address) = relay(Default::default()); |
| 473 | let ada = Live::start(hello("Ada"), &room, None, Some(&url), || {}).unwrap(); | 483 | let ada = Live::start(hello("Ada"), &room, None, Some(&url), |_| {}).unwrap(); |
| 474 | ada.set_presence(caret(1)); | 484 | ada.set_presence(caret(1)); |
| 475 | // Ada's opening, hello and first presence reach Grace; the next is tampered with. | 485 | // Ada's opening, hello and first presence reach Grace; the next is tampered with. |
| 476 | let relay = malicious(address, tamper, 3); | 486 | let relay = malicious(address, tamper, 3); |
crates/notebook/src/live/wire.rs+249-3| ... | @@ -20,11 +20,60 @@ pub mod kind { | ... | @@ -20,11 +20,60 @@ pub mod kind { |
| 20 | pub const HELLO: u16 = 1; | 20 | pub const HELLO: u16 = 1; |
| 21 | /// Keeps a quiet connection open; it says nothing else. | 21 | /// Keeps a quiet connection open; it says nothing else. |
| 22 | pub const PING: u16 = 2; | 22 | pub const PING: u16 = 2; |
| 23 | pub const BYE: u16 = 3; | ||
| 23 | pub const PRESENCE: u16 = 16; | 24 | pub const PRESENCE: u16 = 16; |
| 25 | /// A host's files changed: `Touched`. | ||
| 26 | pub const TOUCHED: u16 = 18; | ||
| 27 | pub const WELCOME: u16 = 32; | ||
| 28 | /// Storage requests to a host, each a `Request` answered by a `Reply`. | ||
| 29 | pub const LIST: u16 = 257; | ||
| 30 | pub const STAMP: u16 = 258; | ||
| 31 | /// A section's consistent image, a chunk at a time. | ||
| 32 | pub const READ: u16 = 259; | ||
| 33 | pub const COMMIT: u16 = 260; | ||
| 34 | pub const CONFIRM: u16 = 261; | ||
| 35 | pub const CREATE: u16 = 262; | ||
| 36 | pub const CREATE_DIRECTORY: u16 = 263; | ||
| 37 | pub const HIDE: u16 = 264; | ||
| 38 | pub const RENAME: u16 = 265; | ||
| 39 | pub const REPLACE: u16 = 266; | ||
| 40 | pub const DELETE: u16 = 267; | ||
| 41 | pub const PLACE: u16 = 268; | ||
| 42 | pub const SUPERSEDE: u16 = 269; | ||
| 43 | /// Any other file as it stands, a chunk at a time. | ||
| 44 | pub const READ_FILE: u16 = 270; | ||
| 45 | pub const EXISTS: u16 = 271; | ||
| 46 | /// A chunk of the bytes a later request carries. | ||
| 47 | pub const PUT: u16 = 272; | ||
| 48 | pub const REPLY: u16 = 511; | ||
| 24 | } | 49 | } |
| 25 | 50 | ||
| 26 | /// The kinds this version reads, as `Hello::kinds` lists them. | 51 | /// The kinds this version reads, as `Hello::kinds` lists them. |
| 27 | pub const KNOWN: &[u16] = &[kind::HELLO, kind::PING, kind::PRESENCE]; | 52 | pub const KNOWN: &[u16] = &[ |
| 53 | kind::HELLO, | ||
| 54 | kind::PING, | ||
| 55 | kind::BYE, | ||
| 56 | kind::PRESENCE, | ||
| 57 | kind::TOUCHED, | ||
| 58 | kind::WELCOME, | ||
| 59 | kind::LIST, | ||
| 60 | kind::STAMP, | ||
| 61 | kind::READ, | ||
| 62 | kind::COMMIT, | ||
| 63 | kind::CONFIRM, | ||
| 64 | kind::CREATE, | ||
| 65 | kind::CREATE_DIRECTORY, | ||
| 66 | kind::HIDE, | ||
| 67 | kind::RENAME, | ||
| 68 | kind::REPLACE, | ||
| 69 | kind::DELETE, | ||
| 70 | kind::PLACE, | ||
| 71 | kind::SUPERSEDE, | ||
| 72 | kind::READ_FILE, | ||
| 73 | kind::EXISTS, | ||
| 74 | kind::PUT, | ||
| 75 | kind::REPLY, | ||
| 76 | ]; | ||
| 28 | 77 | ||
| 29 | /// The first message each way, and the only one with a name and a picture. | 78 | /// The first message each way, and the only one with a name and a picture. |
| 30 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | 79 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| ... | @@ -44,6 +93,9 @@ pub struct Hello { | ... | @@ -44,6 +93,9 @@ pub struct Hello { |
| 44 | /// The message kinds the sender reads; a request waits for its kind to be listed. | 93 | /// The message kinds the sender reads; a request waits for its kind to be listed. |
| 45 | #[n(4)] | 94 | #[n(4)] |
| 46 | pub kinds: Vec<u16>, | 95 | pub kinds: Vec<u16>, |
| 96 | /// The share this peer hosts, whose storage requests it answers. | ||
| 97 | #[cbor(n(5), with = "minicbor::bytes")] | ||
| 98 | pub serves: Option<[u8; 16]>, | ||
| 47 | } | 99 | } |
| 48 | 100 | ||
| 49 | impl Hello { | 101 | impl Hello { |
| ... | @@ -57,6 +109,7 @@ impl Hello { | ... | @@ -57,6 +109,7 @@ impl Hello { |
| 57 | picture, | 109 | picture, |
| 58 | app: format!("Snowbound {}", env!("CARGO_PKG_VERSION")), | 110 | app: format!("Snowbound {}", env!("CARGO_PKG_VERSION")), |
| 59 | kinds: KNOWN.to_vec(), | 111 | kinds: KNOWN.to_vec(), |
| 112 | serves: None, | ||
| 60 | }) | 113 | }) |
| 61 | } | 114 | } |
| 62 | } | 115 | } |
| ... | @@ -123,6 +176,194 @@ impl From<Guid> for onestore::ExGuid { | ... | @@ -123,6 +176,194 @@ impl From<Guid> for onestore::ExGuid { |
| 123 | } | 176 | } |
| 124 | } | 177 | } |
| 125 | 178 | ||
| 179 | /// Why a peer leaves: `left`, or `stopped` for a host that stopped sharing. | ||
| 180 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | ||
| 181 | #[cbor(map)] | ||
| 182 | pub struct Bye { | ||
| 183 | #[n(0)] | ||
| 184 | pub reason: String, | ||
| 185 | } | ||
| 186 | |||
| 187 | /// What the host of a share gives a peer that knew its code: the share's room, and names to | ||
| 188 | /// show it by. | ||
| 189 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | ||
| 190 | #[cbor(map)] | ||
| 191 | pub struct Welcome { | ||
| 192 | #[cbor(n(0), with = "minicbor::bytes")] | ||
| 193 | pub share: [u8; 16], | ||
| 194 | #[cbor(n(1), with = "minicbor::bytes")] | ||
| 195 | pub secret: [u8; 16], | ||
| 196 | #[n(2)] | ||
| 197 | pub notebook: String, | ||
| 198 | /// The host's name for itself, as `Hello::name`. | ||
| 199 | #[n(3)] | ||
| 200 | pub host: String, | ||
| 201 | } | ||
| 202 | |||
| 203 | /// Paths a host's files changed at, by catalog path; `""` is the notebook's folder. | ||
| 204 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] | ||
| 205 | #[cbor(map)] | ||
| 206 | pub struct Touched { | ||
| 207 | #[n(0)] | ||
| 208 | pub paths: Vec<String>, | ||
| 209 | } | ||
| 210 | |||
| 211 | /// A storage request; its kind names the verb, and the verb what it carries. | ||
| 212 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] | ||
| 213 | #[cbor(map)] | ||
| 214 | pub struct Request { | ||
| 215 | /// Names the reply; a `PUT`'s names the bytes a later request carries. | ||
| 216 | #[n(0)] | ||
| 217 | pub id: u64, | ||
| 218 | /// A catalog path. | ||
| 219 | #[n(1)] | ||
| 220 | pub path: String, | ||
| 221 | /// A rename's or replacement's target, or the file a supersession puts in place. | ||
| 222 | #[n(2)] | ||
| 223 | pub to: Option<String>, | ||
| 224 | #[n(3)] | ||
| 225 | pub offset: Option<u64>, | ||
| 226 | #[n(4)] | ||
| 227 | pub limit: Option<u64>, | ||
| 228 | #[cbor(n(5), with = "minicbor::bytes")] | ||
| 229 | pub bytes: Option<Vec<u8>>, | ||
| 230 | /// A confirmation's or supersession's base. | ||
| 231 | #[n(6)] | ||
| 232 | pub stamp: Option<WireStamp>, | ||
| 233 | #[cbor(n(7), with = "minicbor::bytes")] | ||
| 234 | pub ancestor: Option<[u8; 16]>, | ||
| 235 | #[n(8)] | ||
| 236 | pub name: Option<String>, | ||
| 237 | /// The `PUT`s, by id, whose bytes this request carries in place of `bytes`; a read's | ||
| 238 | /// snapshot after its first chunk. | ||
| 239 | #[n(9)] | ||
| 240 | pub handle: Option<u64>, | ||
| 241 | } | ||
| 242 | |||
| 243 | /// A request's answer: a failure, or what the verb gives. | ||
| 244 | #[derive(Clone, Debug, Default, PartialEq, Encode, Decode)] | ||
| 245 | #[cbor(map)] | ||
| 246 | pub struct Reply { | ||
| 247 | #[n(0)] | ||
| 248 | pub id: u64, | ||
| 249 | #[n(1)] | ||
| 250 | pub failure: Option<Failure>, | ||
| 251 | #[cbor(n(2), with = "minicbor::bytes")] | ||
| 252 | pub bytes: Option<Vec<u8>>, | ||
| 253 | /// A read's whole length. | ||
| 254 | #[n(3)] | ||
| 255 | pub length: Option<u64>, | ||
| 256 | #[n(4)] | ||
| 257 | pub stamp: Option<WireStamp>, | ||
| 258 | #[n(5)] | ||
| 259 | pub entries: Option<Vec<WireEntry>>, | ||
| 260 | #[n(6)] | ||
| 261 | pub exists: Option<bool>, | ||
| 262 | /// The snapshot a read's later chunks come from. | ||
| 263 | #[n(7)] | ||
| 264 | pub handle: Option<u64>, | ||
| 265 | } | ||
| 266 | |||
| 267 | /// Why a request failed: an `io::ErrorKind` as `error_kind` numbers it, and for a commit, | ||
| 268 | /// how far it got (`commit_state`). | ||
| 269 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | ||
| 270 | #[cbor(map)] | ||
| 271 | pub struct Failure { | ||
| 272 | #[n(0)] | ||
| 273 | pub kind: u16, | ||
| 274 | #[n(1)] | ||
| 275 | pub message: String, | ||
| 276 | #[n(2)] | ||
| 277 | pub state: Option<u8>, | ||
| 278 | } | ||
| 279 | |||
| 280 | /// An `onestore::Stamp`. | ||
| 281 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | ||
| 282 | #[cbor(map)] | ||
| 283 | pub struct WireStamp { | ||
| 284 | #[cbor(n(0), with = "minicbor::bytes")] | ||
| 285 | pub header: Vec<u8>, | ||
| 286 | #[n(1)] | ||
| 287 | pub length: u64, | ||
| 288 | } | ||
| 289 | |||
| 290 | impl From<&onestore::Stamp> for WireStamp { | ||
| 291 | fn from(stamp: &onestore::Stamp) -> Self { | ||
| 292 | Self { | ||
| 293 | header: stamp.header.to_vec(), | ||
| 294 | length: stamp.length, | ||
| 295 | } | ||
| 296 | } | ||
| 297 | } | ||
| 298 | |||
| 299 | impl TryFrom<&WireStamp> for onestore::Stamp { | ||
| 300 | type Error = io::Error; | ||
| 301 | |||
| 302 | fn try_from(stamp: &WireStamp) -> io::Result<Self> { | ||
| 303 | Ok(Self { | ||
| 304 | header: stamp | ||
| 305 | .header | ||
| 306 | .as_slice() | ||
| 307 | .try_into() | ||
| 308 | .map_err(|_| invalid("A stamp's header is 1024 bytes"))?, | ||
| 309 | length: stamp.length, | ||
| 310 | }) | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | /// A folder's entry, as `discover::Entry`. | ||
| 315 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | ||
| 316 | #[cbor(map)] | ||
| 317 | pub struct WireEntry { | ||
| 318 | #[n(0)] | ||
| 319 | pub name: String, | ||
| 320 | /// 0 a file, 1 a folder, 2 anything else, 3 a file kept elsewhere. | ||
| 321 | #[n(1)] | ||
| 322 | pub kind: u8, | ||
| 323 | #[n(2)] | ||
| 324 | pub size: u64, | ||
| 325 | #[n(3)] | ||
| 326 | pub modified: u64, | ||
| 327 | } | ||
| 328 | |||
| 329 | /// The `io::ErrorKind`s a failure names, by number; any other is `Other`. | ||
| 330 | const ERROR_KINDS: [io::ErrorKind; 20] = [ | ||
| 331 | io::ErrorKind::Other, | ||
| 332 | io::ErrorKind::NotFound, | ||
| 333 | io::ErrorKind::PermissionDenied, | ||
| 334 | io::ErrorKind::AlreadyExists, | ||
| 335 | io::ErrorKind::InvalidInput, | ||
| 336 | io::ErrorKind::InvalidData, | ||
| 337 | io::ErrorKind::TimedOut, | ||
| 338 | io::ErrorKind::WouldBlock, | ||
| 339 | io::ErrorKind::ResourceBusy, | ||
| 340 | io::ErrorKind::Unsupported, | ||
| 341 | io::ErrorKind::FileTooLarge, | ||
| 342 | io::ErrorKind::NotConnected, | ||
| 343 | io::ErrorKind::ReadOnlyFilesystem, | ||
| 344 | io::ErrorKind::DirectoryNotEmpty, | ||
| 345 | io::ErrorKind::NotADirectory, | ||
| 346 | io::ErrorKind::IsADirectory, | ||
| 347 | io::ErrorKind::StorageFull, | ||
| 348 | io::ErrorKind::UnexpectedEof, | ||
| 349 | io::ErrorKind::Interrupted, | ||
| 350 | io::ErrorKind::BrokenPipe, | ||
| 351 | ]; | ||
| 352 | |||
| 353 | pub fn error_number(kind: io::ErrorKind) -> u16 { | ||
| 354 | ERROR_KINDS | ||
| 355 | .iter() | ||
| 356 | .position(|known| *known == kind) | ||
| 357 | .unwrap_or(0) as u16 | ||
| 358 | } | ||
| 359 | |||
| 360 | pub fn error_kind(number: u16) -> io::ErrorKind { | ||
| 361 | ERROR_KINDS | ||
| 362 | .get(usize::from(number)) | ||
| 363 | .copied() | ||
| 364 | .unwrap_or(io::ErrorKind::Other) | ||
| 365 | } | ||
| 366 | |||
| 126 | /// The opening, sent in the clear by the side that connected and answered by the other. | 367 | /// The opening, sent in the clear by the side that connected and answered by the other. |
| 127 | #[derive(Encode, Decode)] | 368 | #[derive(Encode, Decode)] |
| 128 | #[cbor(map)] | 369 | #[cbor(map)] |
| ... | @@ -161,8 +402,13 @@ impl Sealer { | ... | @@ -161,8 +402,13 @@ impl Sealer { |
| 161 | kind: u16, | 402 | kind: u16, |
| 162 | body: &impl Encode<()>, | 403 | body: &impl Encode<()>, |
| 163 | ) -> io::Result<()> { | 404 | ) -> io::Result<()> { |
| 164 | let mut clear = kind.to_be_bytes().to_vec(); | 405 | let body = minicbor::to_vec(body).map_err(io::Error::other)?; |
| 165 | minicbor::encode(body, &mut clear).map_err(io::Error::other)?; | 406 | self.send_encoded(to, kind, &body) |
| 407 | } | ||
| 408 | |||
| 409 | /// `send` for a body already encoded. | ||
| 410 | pub fn send_encoded(&mut self, to: &mut impl Write, kind: u16, body: &[u8]) -> io::Result<()> { | ||
| 411 | let clear = [&kind.to_be_bytes()[..], body].concat(); | ||
| 166 | let number = self.count.to_be_bytes(); | 412 | let number = self.count.to_be_bytes(); |
| 167 | let sealed = self | 413 | let sealed = self |
| 168 | .cipher | 414 | .cipher |
crates/notebook/src/live/words.txt created+1296| ... | @@ -0,0 +1,1296 @@ | ||
| 1 | acid | ||
| 2 | acorn | ||
| 3 | acre | ||
| 4 | acts | ||
| 5 | afar | ||
| 6 | affix | ||
| 7 | aged | ||
| 8 | agent | ||
| 9 | agile | ||
| 10 | aging | ||
| 11 | agony | ||
| 12 | ahead | ||
| 13 | aide | ||
| 14 | aids | ||
| 15 | aim | ||
| 16 | ajar | ||
| 17 | alarm | ||
| 18 | alias | ||
| 19 | alibi | ||
| 20 | alien | ||
| 21 | alike | ||
| 22 | alive | ||
| 23 | aloe | ||
| 24 | aloft | ||
| 25 | aloha | ||
| 26 | alone | ||
| 27 | amend | ||
| 28 | amino | ||
| 29 | ample | ||
| 30 | amuse | ||
| 31 | angel | ||
| 32 | anger | ||
| 33 | angle | ||
| 34 | ankle | ||
| 35 | apple | ||
| 36 | april | ||
| 37 | apron | ||
| 38 | aqua | ||
| 39 | area | ||
| 40 | arena | ||
| 41 | argue | ||
| 42 | arise | ||
| 43 | armed | ||
| 44 | armor | ||
| 45 | army | ||
| 46 | aroma | ||
| 47 | array | ||
| 48 | arson | ||
| 49 | art | ||
| 50 | ashen | ||
| 51 | ashes | ||
| 52 | atlas | ||
| 53 | atom | ||
| 54 | attic | ||
| 55 | audio | ||
| 56 | avert | ||
| 57 | avoid | ||
| 58 | awake | ||
| 59 | award | ||
| 60 | awoke | ||
| 61 | axis | ||
| 62 | bacon | ||
| 63 | badge | ||
| 64 | bagel | ||
| 65 | baggy | ||
| 66 | baked | ||
| 67 | baker | ||
| 68 | balmy | ||
| 69 | banjo | ||
| 70 | barge | ||
| 71 | barn | ||
| 72 | bash | ||
| 73 | basil | ||
| 74 | bask | ||
| 75 | batch | ||
| 76 | bath | ||
| 77 | baton | ||
| 78 | bats | ||
| 79 | blade | ||
| 80 | blank | ||
| 81 | blast | ||
| 82 | blaze | ||
| 83 | bleak | ||
| 84 | blend | ||
| 85 | bless | ||
| 86 | blimp | ||
| 87 | blink | ||
| 88 | bloat | ||
| 89 | blob | ||
| 90 | blog | ||
| 91 | blot | ||
| 92 | blunt | ||
| 93 | blurt | ||
| 94 | blush | ||
| 95 | boast | ||
| 96 | boat | ||
| 97 | body | ||
| 98 | boil | ||
| 99 | bok | ||
| 100 | bolt | ||
| 101 | boned | ||
| 102 | boney | ||
| 103 | bonus | ||
| 104 | bony | ||
| 105 | book | ||
| 106 | booth | ||
| 107 | boots | ||
| 108 | boss | ||
| 109 | botch | ||
| 110 | both | ||
| 111 | boxer | ||
| 112 | breed | ||
| 113 | bribe | ||
| 114 | brick | ||
| 115 | bride | ||
| 116 | brim | ||
| 117 | bring | ||
| 118 | brink | ||
| 119 | brisk | ||
| 120 | broad | ||
| 121 | broil | ||
| 122 | broke | ||
| 123 | brook | ||
| 124 | broom | ||
| 125 | brush | ||
| 126 | buck | ||
| 127 | bud | ||
| 128 | buggy | ||
| 129 | bulge | ||
| 130 | bulk | ||
| 131 | bully | ||
| 132 | bunch | ||
| 133 | bunny | ||
| 134 | bunt | ||
| 135 | bush | ||
| 136 | bust | ||
| 137 | busy | ||
| 138 | buzz | ||
| 139 | cable | ||
| 140 | cache | ||
| 141 | cadet | ||
| 142 | cage | ||
| 143 | cake | ||
| 144 | calm | ||
| 145 | cameo | ||
| 146 | canal | ||
| 147 | candy | ||
| 148 | cane | ||
| 149 | canon | ||
| 150 | cape | ||
| 151 | card | ||
| 152 | cargo | ||
| 153 | carol | ||
| 154 | carry | ||
| 155 | carve | ||
| 156 | case | ||
| 157 | cash | ||
| 158 | cause | ||
| 159 | cedar | ||
| 160 | chain | ||
| 161 | chair | ||
| 162 | chant | ||
| 163 | chaos | ||
| 164 | charm | ||
| 165 | chase | ||
| 166 | cheek | ||
| 167 | cheer | ||
| 168 | chef | ||
| 169 | chess | ||
| 170 | chest | ||
| 171 | chew | ||
| 172 | chief | ||
| 173 | chili | ||
| 174 | chill | ||
| 175 | chip | ||
| 176 | chomp | ||
| 177 | chop | ||
| 178 | chow | ||
| 179 | chuck | ||
| 180 | chump | ||
| 181 | chunk | ||
| 182 | churn | ||
| 183 | chute | ||
| 184 | cider | ||
| 185 | cinch | ||
| 186 | city | ||
| 187 | civic | ||
| 188 | civil | ||
| 189 | clad | ||
| 190 | claim | ||
| 191 | clamp | ||
| 192 | clap | ||
| 193 | clash | ||
| 194 | clasp | ||
| 195 | class | ||
| 196 | claw | ||
| 197 | clay | ||
| 198 | clean | ||
| 199 | clear | ||
| 200 | cleat | ||
| 201 | cleft | ||
| 202 | clerk | ||
| 203 | click | ||
| 204 | cling | ||
| 205 | clink | ||
| 206 | clip | ||
| 207 | cloak | ||
| 208 | clock | ||
| 209 | clone | ||
| 210 | cloth | ||
| 211 | cloud | ||
| 212 | clump | ||
| 213 | coach | ||
| 214 | coast | ||
| 215 | coat | ||
| 216 | cod | ||
| 217 | coil | ||
| 218 | coke | ||
| 219 | cola | ||
| 220 | cold | ||
| 221 | colt | ||
| 222 | coma | ||
| 223 | come | ||
| 224 | comic | ||
| 225 | comma | ||
| 226 | cone | ||
| 227 | cope | ||
| 228 | copy | ||
| 229 | coral | ||
| 230 | cork | ||
| 231 | cost | ||
| 232 | cot | ||
| 233 | couch | ||
| 234 | cough | ||
| 235 | cover | ||
| 236 | cozy | ||
| 237 | craft | ||
| 238 | cramp | ||
| 239 | crane | ||
| 240 | crank | ||
| 241 | crate | ||
| 242 | crave | ||
| 243 | crawl | ||
| 244 | crazy | ||
| 245 | creme | ||
| 246 | crepe | ||
| 247 | crept | ||
| 248 | crib | ||
| 249 | cried | ||
| 250 | crisp | ||
| 251 | crook | ||
| 252 | crop | ||
| 253 | cross | ||
| 254 | crowd | ||
| 255 | crown | ||
| 256 | crumb | ||
| 257 | crush | ||
| 258 | crust | ||
| 259 | cub | ||
| 260 | cult | ||
| 261 | cupid | ||
| 262 | cure | ||
| 263 | curl | ||
| 264 | curry | ||
| 265 | curse | ||
| 266 | curve | ||
| 267 | curvy | ||
| 268 | cushy | ||
| 269 | cut | ||
| 270 | cycle | ||
| 271 | dab | ||
| 272 | dad | ||
| 273 | daily | ||
| 274 | dairy | ||
| 275 | daisy | ||
| 276 | dance | ||
| 277 | dandy | ||
| 278 | darn | ||
| 279 | dart | ||
| 280 | dash | ||
| 281 | data | ||
| 282 | date | ||
| 283 | dawn | ||
| 284 | deaf | ||
| 285 | deal | ||
| 286 | dean | ||
| 287 | debit | ||
| 288 | debt | ||
| 289 | debug | ||
| 290 | decaf | ||
| 291 | decal | ||
| 292 | decay | ||
| 293 | deck | ||
| 294 | decor | ||
| 295 | decoy | ||
| 296 | deed | ||
| 297 | delay | ||
| 298 | denim | ||
| 299 | dense | ||
| 300 | dent | ||
| 301 | depth | ||
| 302 | derby | ||
| 303 | desk | ||
| 304 | dial | ||
| 305 | diary | ||
| 306 | dice | ||
| 307 | dig | ||
| 308 | dill | ||
| 309 | dime | ||
| 310 | dimly | ||
| 311 | diner | ||
| 312 | dingy | ||
| 313 | disco | ||
| 314 | dish | ||
| 315 | disk | ||
| 316 | ditch | ||
| 317 | ditzy | ||
| 318 | dizzy | ||
| 319 | dock | ||
| 320 | dodge | ||
| 321 | doing | ||
| 322 | doll | ||
| 323 | dome | ||
| 324 | donor | ||
| 325 | donut | ||
| 326 | dose | ||
| 327 | dot | ||
| 328 | dove | ||
| 329 | down | ||
| 330 | dowry | ||
| 331 | doze | ||
| 332 | drab | ||
| 333 | drama | ||
| 334 | drank | ||
| 335 | draw | ||
| 336 | dress | ||
| 337 | dried | ||
| 338 | drift | ||
| 339 | drill | ||
| 340 | drive | ||
| 341 | drone | ||
| 342 | droop | ||
| 343 | drove | ||
| 344 | drown | ||
| 345 | drum | ||
| 346 | dry | ||
| 347 | duck | ||
| 348 | duct | ||
| 349 | dude | ||
| 350 | dug | ||
| 351 | duke | ||
| 352 | duo | ||
| 353 | dusk | ||
| 354 | dust | ||
| 355 | duty | ||
| 356 | dwarf | ||
| 357 | dwell | ||
| 358 | eagle | ||
| 359 | early | ||
| 360 | earth | ||
| 361 | easel | ||
| 362 | east | ||
| 363 | eaten | ||
| 364 | eats | ||
| 365 | ebay | ||
| 366 | ebony | ||
| 367 | ebook | ||
| 368 | echo | ||
| 369 | edge | ||
| 370 | eel | ||
| 371 | eject | ||
| 372 | elbow | ||
| 373 | elder | ||
| 374 | elf | ||
| 375 | elk | ||
| 376 | elm | ||
| 377 | elope | ||
| 378 | elude | ||
| 379 | elves | ||
| 380 | |||
| 381 | emit | ||
| 382 | empty | ||
| 383 | emu | ||
| 384 | enter | ||
| 385 | entry | ||
| 386 | envoy | ||
| 387 | equal | ||
| 388 | erase | ||
| 389 | error | ||
| 390 | erupt | ||
| 391 | essay | ||
| 392 | etch | ||
| 393 | evade | ||
| 394 | even | ||
| 395 | evict | ||
| 396 | evil | ||
| 397 | evoke | ||
| 398 | exact | ||
| 399 | exit | ||
| 400 | fable | ||
| 401 | faced | ||
| 402 | fact | ||
| 403 | fade | ||
| 404 | fall | ||
| 405 | false | ||
| 406 | fancy | ||
| 407 | fang | ||
| 408 | fax | ||
| 409 | feast | ||
| 410 | feed | ||
| 411 | femur | ||
| 412 | fence | ||
| 413 | fend | ||
| 414 | ferry | ||
| 415 | fetal | ||
| 416 | fetch | ||
| 417 | fever | ||
| 418 | fiber | ||
| 419 | fifth | ||
| 420 | fifty | ||
| 421 | film | ||
| 422 | filth | ||
| 423 | final | ||
| 424 | finch | ||
| 425 | fit | ||
| 426 | five | ||
| 427 | flag | ||
| 428 | flaky | ||
| 429 | flame | ||
| 430 | flap | ||
| 431 | flask | ||
| 432 | fled | ||
| 433 | flick | ||
| 434 | fling | ||
| 435 | flint | ||
| 436 | flip | ||
| 437 | flirt | ||
| 438 | float | ||
| 439 | flock | ||
| 440 | flop | ||
| 441 | floss | ||
| 442 | flyer | ||
| 443 | foam | ||
| 444 | foe | ||
| 445 | fog | ||
| 446 | foil | ||
| 447 | folic | ||
| 448 | folk | ||
| 449 | food | ||
| 450 | fool | ||
| 451 | found | ||
| 452 | fox | ||
| 453 | foyer | ||
| 454 | frail | ||
| 455 | frame | ||
| 456 | fray | ||
| 457 | fresh | ||
| 458 | fried | ||
| 459 | frill | ||
| 460 | frisk | ||
| 461 | from | ||
| 462 | front | ||
| 463 | frost | ||
| 464 | froth | ||
| 465 | frown | ||
| 466 | froze | ||
| 467 | fruit | ||
| 468 | gag | ||
| 469 | gains | ||
| 470 | gala | ||
| 471 | game | ||
| 472 | gap | ||
| 473 | gas | ||
| 474 | gave | ||
| 475 | gear | ||
| 476 | gecko | ||
| 477 | geek | ||
| 478 | gem | ||
| 479 | genre | ||
| 480 | gift | ||
| 481 | gig | ||
| 482 | gills | ||
| 483 | given | ||
| 484 | giver | ||
| 485 | glad | ||
| 486 | glass | ||
| 487 | glide | ||
| 488 | gloss | ||
| 489 | glove | ||
| 490 | glow | ||
| 491 | glue | ||
| 492 | goal | ||
| 493 | going | ||
| 494 | golf | ||
| 495 | gong | ||
| 496 | good | ||
| 497 | gooey | ||
| 498 | goofy | ||
| 499 | gore | ||
| 500 | gown | ||
| 501 | grab | ||
| 502 | grain | ||
| 503 | grant | ||
| 504 | grape | ||
| 505 | graph | ||
| 506 | grasp | ||
| 507 | grass | ||
| 508 | grave | ||
| 509 | gravy | ||
| 510 | gray | ||
| 511 | green | ||
| 512 | greet | ||
| 513 | grew | ||
| 514 | grid | ||
| 515 | grief | ||
| 516 | grill | ||
| 517 | grip | ||
| 518 | grit | ||
| 519 | groom | ||
| 520 | grope | ||
| 521 | growl | ||
| 522 | grub | ||
| 523 | grunt | ||
| 524 | guide | ||
| 525 | gulf | ||
| 526 | gulp | ||
| 527 | gummy | ||
| 528 | guru | ||
| 529 | gush | ||
| 530 | gut | ||
| 531 | guy | ||
| 532 | habit | ||
| 533 | half | ||
| 534 | halo | ||
| 535 | halt | ||
| 536 | happy | ||
| 537 | harm | ||
| 538 | hash | ||
| 539 | hasty | ||
| 540 | hatch | ||
| 541 | hate | ||
| 542 | haven | ||
| 543 | hazel | ||
| 544 | hazy | ||
| 545 | heap | ||
| 546 | heat | ||
| 547 | heave | ||
| 548 | hedge | ||
| 549 | hefty | ||
| 550 | help | ||
| 551 | herbs | ||
| 552 | hers | ||
| 553 | hub | ||
| 554 | hug | ||
| 555 | hula | ||
| 556 | hull | ||
| 557 | human | ||
| 558 | humid | ||
| 559 | hump | ||
| 560 | hung | ||
| 561 | hunk | ||
| 562 | hunt | ||
| 563 | hurry | ||
| 564 | hurt | ||
| 565 | hush | ||
| 566 | hut | ||
| 567 | ice | ||
| 568 | icing | ||
| 569 | icon | ||
| 570 | icy | ||
| 571 | igloo | ||
| 572 | image | ||
| 573 | ion | ||
| 574 | iron | ||
| 575 | islam | ||
| 576 | issue | ||
| 577 | item | ||
| 578 | ivory | ||
| 579 | ivy | ||
| 580 | jab | ||
| 581 | jam | ||
| 582 | jaws | ||
| 583 | jazz | ||
| 584 | jeep | ||
| 585 | jelly | ||
| 586 | jet | ||
| 587 | jiffy | ||
| 588 | job | ||
| 589 | jog | ||
| 590 | jolly | ||
| 591 | jolt | ||
| 592 | jot | ||
| 593 | joy | ||
| 594 | judge | ||
| 595 | juice | ||
| 596 | juicy | ||
| 597 | july | ||
| 598 | jumbo | ||
| 599 | jump | ||
| 600 | junky | ||
| 601 | juror | ||
| 602 | jury | ||
| 603 | keep | ||
| 604 | keg | ||
| 605 | kept | ||
| 606 | kick | ||
| 607 | kilt | ||
| 608 | king | ||
| 609 | kite | ||
| 610 | kitty | ||
| 611 | kiwi | ||
| 612 | knee | ||
| 613 | knelt | ||
| 614 | koala | ||
| 615 | kung | ||
| 616 | ladle | ||
| 617 | lady | ||
| 618 | lair | ||
| 619 | lake | ||
| 620 | lance | ||
| 621 | land | ||
| 622 | lapel | ||
| 623 | large | ||
| 624 | lash | ||
| 625 | lasso | ||
| 626 | last | ||
| 627 | latch | ||
| 628 | late | ||
| 629 | lazy | ||
| 630 | left | ||
| 631 | legal | ||
| 632 | lemon | ||
| 633 | lend | ||
| 634 | lens | ||
| 635 | lent | ||
| 636 | level | ||
| 637 | lever | ||
| 638 | lid | ||
| 639 | life | ||
| 640 | lift | ||
| 641 | lilac | ||
| 642 | lily | ||
| 643 | limb | ||
| 644 | limes | ||
| 645 | line | ||
| 646 | lint | ||
| 647 | lion | ||
| 648 | lip | ||
| 649 | list | ||
| 650 | lived | ||
| 651 | liver | ||
| 652 | lunar | ||
| 653 | lunch | ||
| 654 | lung | ||
| 655 | lurch | ||
| 656 | lure | ||
| 657 | lurk | ||
| 658 | lying | ||
| 659 | lyric | ||
| 660 | mace | ||
| 661 | maker | ||
| 662 | malt | ||
| 663 | mama | ||
| 664 | mango | ||
| 665 | manor | ||
| 666 | many | ||
| 667 | map | ||
| 668 | march | ||
| 669 | mardi | ||
| 670 | marry | ||
| 671 | mash | ||
| 672 | match | ||
| 673 | mate | ||
| 674 | math | ||
| 675 | moan | ||
| 676 | mocha | ||
| 677 | moist | ||
| 678 | mold | ||
| 679 | mom | ||
| 680 | moody | ||
| 681 | mop | ||
| 682 | morse | ||
| 683 | most | ||
| 684 | motor | ||
| 685 | motto | ||
| 686 | mount | ||
| 687 | mouse | ||
| 688 | mousy | ||
| 689 | mouth | ||
| 690 | move | ||
| 691 | movie | ||
| 692 | mower | ||
| 693 | mud | ||
| 694 | mug | ||
| 695 | mulch | ||
| 696 | mule | ||
| 697 | mull | ||
| 698 | mumbo | ||
| 699 | mummy | ||
| 700 | mural | ||
| 701 | muse | ||
| 702 | music | ||
| 703 | musky | ||
| 704 | mute | ||
| 705 | nacho | ||
| 706 | nag | ||
| 707 | nail | ||
| 708 | name | ||
| 709 | nanny | ||
| 710 | nap | ||
| 711 | navy | ||
| 712 | near | ||
| 713 | neat | ||
| 714 | neon | ||
| 715 | nerd | ||
| 716 | nest | ||
| 717 | net | ||
| 718 | next | ||
| 719 | niece | ||
| 720 | ninth | ||
| 721 | nutty | ||
| 722 | oak | ||
| 723 | oasis | ||
| 724 | oat | ||
| 725 | ocean | ||
| 726 | oil | ||
| 727 | old | ||
| 728 | olive | ||
| 729 | omen | ||
| 730 | onion | ||
| 731 | only | ||
| 732 | ooze | ||
| 733 | opal | ||
| 734 | open | ||
| 735 | opera | ||
| 736 | opt | ||
| 737 | otter | ||
| 738 | ouch | ||
| 739 | ounce | ||
| 740 | outer | ||
| 741 | oval | ||
| 742 | oven | ||
| 743 | owl | ||
| 744 | ozone | ||
| 745 | pace | ||
| 746 | pagan | ||
| 747 | pager | ||
| 748 | palm | ||
| 749 | panda | ||
| 750 | panic | ||
| 751 | pants | ||
| 752 | panty | ||
| 753 | paper | ||
| 754 | park | ||
| 755 | party | ||
| 756 | pasta | ||
| 757 | patch | ||
| 758 | path | ||
| 759 | patio | ||
| 760 | payer | ||
| 761 | pecan | ||
| 762 | penny | ||
| 763 | pep | ||
| 764 | perch | ||
| 765 | perky | ||
| 766 | perm | ||
| 767 | pest | ||
| 768 | petal | ||
| 769 | petri | ||
| 770 | petty | ||
| 771 | photo | ||
| 772 | plank | ||
| 773 | plant | ||
| 774 | plaza | ||
| 775 | plead | ||
| 776 | plot | ||
| 777 | plow | ||
| 778 | pluck | ||
| 779 | plug | ||
| 780 | plus | ||
| 781 | poach | ||
| 782 | pod | ||
| 783 | poem | ||
| 784 | poet | ||
| 785 | pogo | ||
| 786 | point | ||
| 787 | poise | ||
| 788 | poker | ||
| 789 | polar | ||
| 790 | polio | ||
| 791 | polka | ||
| 792 | polo | ||
| 793 | pond | ||
| 794 | pony | ||
| 795 | poppy | ||
| 796 | pork | ||
| 797 | poser | ||
| 798 | pouch | ||
| 799 | pound | ||
| 800 | pout | ||
| 801 | power | ||
| 802 | prank | ||
| 803 | press | ||
| 804 | |||
| 805 | prior | ||
| 806 | prism | ||
| 807 | prize | ||
| 808 | probe | ||
| 809 | prong | ||
| 810 | proof | ||
| 811 | props | ||
| 812 | prude | ||
| 813 | prune | ||
| 814 | pry | ||
| 815 | pug | ||
| 816 | pull | ||
| 817 | pulp | ||
| 818 | pulse | ||
| 819 | puma | ||
| 820 | punch | ||
| 821 | punk | ||
| 822 | pupil | ||
| 823 | puppy | ||
| 824 | purr | ||
| 825 | purse | ||
| 826 | push | ||
| 827 | putt | ||
| 828 | quack | ||
| 829 | quake | ||
| 830 | query | ||
| 831 | quiet | ||
| 832 | quill | ||
| 833 | quilt | ||
| 834 | quit | ||
| 835 | quota | ||
| 836 | quote | ||
| 837 | rabid | ||
| 838 | race | ||
| 839 | rack | ||
| 840 | radar | ||
| 841 | radio | ||
| 842 | raft | ||
| 843 | rage | ||
| 844 | raid | ||
| 845 | rail | ||
| 846 | rake | ||
| 847 | rally | ||
| 848 | ramp | ||
| 849 | ranch | ||
| 850 | range | ||
| 851 | rank | ||
| 852 | rant | ||
| 853 | rash | ||
| 854 | raven | ||
| 855 | reach | ||
| 856 | react | ||
| 857 | ream | ||
| 858 | rebel | ||
| 859 | recap | ||
| 860 | relax | ||
| 861 | relay | ||
| 862 | relic | ||
| 863 | remix | ||
| 864 | repay | ||
| 865 | repel | ||
| 866 | reply | ||
| 867 | rerun | ||
| 868 | reset | ||
| 869 | rhyme | ||
| 870 | rice | ||
| 871 | rich | ||
| 872 | ride | ||
| 873 | rigid | ||
| 874 | rigor | ||
| 875 | rinse | ||
| 876 | riot | ||
| 877 | ripen | ||
| 878 | rise | ||
| 879 | risk | ||
| 880 | ritzy | ||
| 881 | rival | ||
| 882 | river | ||
| 883 | roast | ||
| 884 | robe | ||
| 885 | robin | ||
| 886 | rock | ||
| 887 | rogue | ||
| 888 | roman | ||
| 889 | romp | ||
| 890 | rope | ||
| 891 | rover | ||
| 892 | royal | ||
| 893 | ruby | ||
| 894 | rug | ||
| 895 | ruin | ||
| 896 | rule | ||
| 897 | runny | ||
| 898 | rush | ||
| 899 | rust | ||
| 900 | rut | ||
| 901 | sadly | ||
| 902 | sage | ||
| 903 | said | ||
| 904 | saint | ||
| 905 | salad | ||
| 906 | salon | ||
| 907 | salsa | ||
| 908 | salt | ||
| 909 | same | ||
| 910 | sandy | ||
| 911 | santa | ||
| 912 | satin | ||
| 913 | sauna | ||
| 914 | saved | ||
| 915 | savor | ||
| 916 | sax | ||
| 917 | say | ||
| 918 | scale | ||
| 919 | scam | ||
| 920 | scan | ||
| 921 | scare | ||
| 922 | scarf | ||
| 923 | scary | ||
| 924 | scoff | ||
| 925 | scold | ||
| 926 | scoop | ||
| 927 | scoot | ||
| 928 | scope | ||
| 929 | score | ||
| 930 | scorn | ||
| 931 | scout | ||
| 932 | scowl | ||
| 933 | scrap | ||
| 934 | scrub | ||
| 935 | scuba | ||
| 936 | scuff | ||
| 937 | sect | ||
| 938 | sedan | ||
| 939 | self | ||
| 940 | send | ||
| 941 | sepia | ||
| 942 | serve | ||
| 943 | set | ||
| 944 | seven | ||
| 945 | shack | ||
| 946 | shade | ||
| 947 | shady | ||
| 948 | shaft | ||
| 949 | shaky | ||
| 950 | sham | ||
| 951 | shape | ||
| 952 | share | ||
| 953 | sharp | ||
| 954 | shed | ||
| 955 | sheep | ||
| 956 | sheet | ||
| 957 | shelf | ||
| 958 | shell | ||
| 959 | shine | ||
| 960 | shiny | ||
| 961 | ship | ||
| 962 | shirt | ||
| 963 | shock | ||
| 964 | shop | ||
| 965 | shore | ||
| 966 | shout | ||
| 967 | shove | ||
| 968 | shown | ||
| 969 | showy | ||
| 970 | shred | ||
| 971 | shrug | ||
| 972 | shun | ||
| 973 | shush | ||
| 974 | shut | ||
| 975 | shy | ||
| 976 | sift | ||
| 977 | silk | ||
| 978 | silly | ||
| 979 | silo | ||
| 980 | sip | ||
| 981 | siren | ||
| 982 | sixth | ||
| 983 | size | ||
| 984 | skate | ||
| 985 | skew | ||
| 986 | skid | ||
| 987 | skier | ||
| 988 | skies | ||
| 989 | skip | ||
| 990 | skirt | ||
| 991 | skit | ||
| 992 | sky | ||
| 993 | slab | ||
| 994 | slack | ||
| 995 | slain | ||
| 996 | slam | ||
| 997 | slang | ||
| 998 | slash | ||
| 999 | slate | ||
| 1000 | slaw | ||
| 1001 | sled | ||
| 1002 | sleek | ||
| 1003 | sleep | ||
| 1004 | sleet | ||
| 1005 | slept | ||
| 1006 | slice | ||
| 1007 | slick | ||
| 1008 | slimy | ||
| 1009 | sling | ||
| 1010 | slip | ||
| 1011 | slit | ||
| 1012 | slob | ||
| 1013 | slot | ||
| 1014 | slug | ||
| 1015 | slum | ||
| 1016 | slurp | ||
| 1017 | slush | ||
| 1018 | small | ||
| 1019 | smash | ||
| 1020 | smell | ||
| 1021 | smile | ||
| 1022 | smirk | ||
| 1023 | smog | ||
| 1024 | snack | ||
| 1025 | snap | ||
| 1026 | snare | ||
| 1027 | snarl | ||
| 1028 | sneak | ||
| 1029 | sneer | ||
| 1030 | sniff | ||
| 1031 | snore | ||
| 1032 | snort | ||
| 1033 | snout | ||
| 1034 | snowy | ||
| 1035 | snub | ||
| 1036 | snuff | ||
| 1037 | speak | ||
| 1038 | speed | ||
| 1039 | spend | ||
| 1040 | spent | ||
| 1041 | spew | ||
| 1042 | spied | ||
| 1043 | spill | ||
| 1044 | spiny | ||
| 1045 | spoil | ||
| 1046 | spoke | ||
| 1047 | spoof | ||
| 1048 | spool | ||
| 1049 | spoon | ||
| 1050 | sport | ||
| 1051 | spot | ||
| 1052 | spout | ||
| 1053 | spray | ||
| 1054 | spree | ||
| 1055 | spur | ||
| 1056 | squad | ||
| 1057 | squat | ||
| 1058 | squid | ||
| 1059 | stack | ||
| 1060 | staff | ||
| 1061 | stage | ||
| 1062 | stain | ||
| 1063 | stall | ||
| 1064 | stamp | ||
| 1065 | stand | ||
| 1066 | stank | ||
| 1067 | stark | ||
| 1068 | start | ||
| 1069 | stash | ||
| 1070 | state | ||
| 1071 | stays | ||
| 1072 | steam | ||
| 1073 | steep | ||
| 1074 | stem | ||
| 1075 | step | ||
| 1076 | stew | ||
| 1077 | stick | ||
| 1078 | sting | ||
| 1079 | stir | ||
| 1080 | stock | ||
| 1081 | stole | ||
| 1082 | stomp | ||
| 1083 | stony | ||
| 1084 | stood | ||
| 1085 | stool | ||
| 1086 | stoop | ||
| 1087 | stop | ||
| 1088 | storm | ||
| 1089 | stout | ||
| 1090 | stove | ||
| 1091 | straw | ||
| 1092 | stray | ||
| 1093 | strut | ||
| 1094 | stuck | ||
| 1095 | stud | ||
| 1096 | stuff | ||
| 1097 | stump | ||
| 1098 | stung | ||
| 1099 | stunt | ||
| 1100 | suds | ||
| 1101 | sugar | ||
| 1102 | sulk | ||
| 1103 | surf | ||
| 1104 | sushi | ||
| 1105 | swab | ||
| 1106 | swan | ||
| 1107 | swarm | ||
| 1108 | sway | ||
| 1109 | swear | ||
| 1110 | sweat | ||
| 1111 | sweep | ||
| 1112 | swell | ||
| 1113 | swept | ||
| 1114 | swim | ||
| 1115 | swing | ||
| 1116 | swipe | ||
| 1117 | swirl | ||
| 1118 | swoop | ||
| 1119 | swore | ||
| 1120 | syrup | ||
| 1121 | tacky | ||
| 1122 | taco | ||
| 1123 | tag | ||
| 1124 | take | ||
| 1125 | tall | ||
| 1126 | talon | ||
| 1127 | tamer | ||
| 1128 | tank | ||
| 1129 | taper | ||
| 1130 | taps | ||
| 1131 | tarot | ||
| 1132 | tart | ||
| 1133 | task | ||
| 1134 | taste | ||
| 1135 | tasty | ||
| 1136 | taunt | ||
| 1137 | thank | ||
| 1138 | thaw | ||
| 1139 | theft | ||
| 1140 | theme | ||
| 1141 | thigh | ||
| 1142 | thing | ||
| 1143 | think | ||
| 1144 | thong | ||
| 1145 | thorn | ||
| 1146 | those | ||
| 1147 | throb | ||
| 1148 | thud | ||
| 1149 | thumb | ||
| 1150 | thump | ||
| 1151 | thus | ||
| 1152 | tiara | ||
| 1153 | tidal | ||
| 1154 | tidy | ||
| 1155 | tiger | ||
| 1156 | tile | ||
| 1157 | tilt | ||
| 1158 | tint | ||
| 1159 | tiny | ||
| 1160 | trace | ||
| 1161 | track | ||
| 1162 | trade | ||
| 1163 | train | ||
| 1164 | trait | ||
| 1165 | trap | ||
| 1166 | trash | ||
| 1167 | tray | ||
| 1168 | treat | ||
| 1169 | tree | ||
| 1170 | trek | ||
| 1171 | trend | ||
| 1172 | trial | ||
| 1173 | tribe | ||
| 1174 | trick | ||
| 1175 | trio | ||
| 1176 | trout | ||
| 1177 | truce | ||
| 1178 | truck | ||
| 1179 | trump | ||
| 1180 | trunk | ||
| 1181 | try | ||
| 1182 | tug | ||
| 1183 | tulip | ||
| 1184 | tummy | ||
| 1185 | turf | ||
| 1186 | tusk | ||
| 1187 | tutor | ||
| 1188 | tutu | ||
| 1189 | tux | ||
| 1190 | tweak | ||
| 1191 | tweet | ||
| 1192 | twice | ||
| 1193 | twine | ||
| 1194 | twins | ||
| 1195 | twirl | ||
| 1196 | twist | ||
| 1197 | uncle | ||
| 1198 | uncut | ||
| 1199 | undo | ||
| 1200 | unify | ||
| 1201 | union | ||
| 1202 | unit | ||
| 1203 | untie | ||
| 1204 | upon | ||
| 1205 | upper | ||
| 1206 | urban | ||
| 1207 | used | ||
| 1208 | user | ||
| 1209 | usher | ||
| 1210 | utter | ||
| 1211 | value | ||
| 1212 | vapor | ||
| 1213 | vegan | ||
| 1214 | venue | ||
| 1215 | verse | ||
| 1216 | vest | ||
| 1217 | veto | ||
| 1218 | vice | ||
| 1219 | video | ||
| 1220 | view | ||
| 1221 | viral | ||
| 1222 | virus | ||
| 1223 | visa | ||
| 1224 | visor | ||
| 1225 | vixen | ||
| 1226 | vocal | ||
| 1227 | voice | ||
| 1228 | void | ||
| 1229 | volt | ||
| 1230 | voter | ||
| 1231 | vowel | ||
| 1232 | wad | ||
| 1233 | wafer | ||
| 1234 | wager | ||
| 1235 | wages | ||
| 1236 | wagon | ||
| 1237 | wake | ||
| 1238 | walk | ||
| 1239 | wand | ||
| 1240 | wasp | ||
| 1241 | watch | ||
| 1242 | water | ||
| 1243 | wavy | ||
| 1244 | wheat | ||
| 1245 | whiff | ||
| 1246 | whole | ||
| 1247 | whoop | ||
| 1248 | wick | ||
| 1249 | widen | ||
| 1250 | widow | ||
| 1251 | width | ||
| 1252 | wife | ||
| 1253 | wifi | ||
| 1254 | wilt | ||
| 1255 | wimp | ||
| 1256 | wind | ||
| 1257 | wing | ||
| 1258 | wink | ||
| 1259 | wipe | ||
| 1260 | wired | ||
| 1261 | wiry | ||
| 1262 | wise | ||
| 1263 | wish | ||
| 1264 | wispy | ||
| 1265 | wok | ||
| 1266 | wolf | ||
| 1267 | womb | ||
| 1268 | wool | ||
| 1269 | woozy | ||
| 1270 | word | ||
| 1271 | work | ||
| 1272 | worry | ||
| 1273 | wound | ||
| 1274 | woven | ||
| 1275 | wrath | ||
| 1276 | wreck | ||
| 1277 | wrist | ||
| 1278 | xerox | ||
| 1279 | yahoo | ||
| 1280 | yam | ||
| 1281 | yard | ||
| 1282 | year | ||
| 1283 | yeast | ||
| 1284 | yelp | ||
| 1285 | yield | ||
| 1286 | yarn | ||
| 1287 | yodel | ||
| 1288 | yoga | ||
| 1289 | yoyo | ||
| 1290 | yummy | ||
| 1291 | zebra | ||
| 1292 | zero | ||
| 1293 | zesty | ||
| 1294 | zippy | ||
| 1295 | zone | ||
| 1296 | zoom | ||
crates/notebook/src/session.rs+75-1| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | //! The application's view of a notebook: sections opened through a local replica that | 1 | //! The application's view of a notebook: sections opened through a local replica that |
| 2 | //! publishes their edits to the section file in the background. | 2 | //! publishes their edits to the section file in the background. |
| 3 | 3 | ||
| 4 | pub use crate::background::{Background, Known}; | 4 | pub use crate::background::{Background, Known, Listener}; |
| 5 | use crate::{ | 5 | use crate::{ |
| 6 | EditStatus, Error, PendingEdit, Remote, Replica, Resolution, Result, SyncWorker, discover, fs, | 6 | EditStatus, Error, PendingEdit, Remote, Replica, Resolution, Result, SyncWorker, discover, fs, |
| 7 | }; | 7 | }; |
| ... | @@ -81,7 +81,11 @@ pub trait Storage: Send + Sync { | ... | @@ -81,7 +81,11 @@ pub trait Storage: Send + Sync { |
| 81 | ) -> Result<discover::Folder>; | 81 | ) -> Result<discover::Folder>; |
| 82 | /// Where the notebook lives, which names its catalog's cache. | 82 | /// Where the notebook lives, which names its catalog's cache. |
| 83 | fn location(&self) -> String; | 83 | fn location(&self) -> String; |
| 84 | /// A folder's entries, as discovery lists them. | ||
| 85 | fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>>; | ||
| 84 | fn exists(&self, path: &str) -> bool; | 86 | fn exists(&self, path: &str) -> bool; |
| 87 | /// A section's or TOC's stamp, without reading its body or coordinating with writers. | ||
| 88 | fn stamp(&self, path: &str) -> io::Result<Stamp>; | ||
| 85 | fn read(&self, path: &str) -> Result<Vec<u8>>; | 89 | fn read(&self, path: &str) -> Result<Vec<u8>>; |
| 86 | /// Reads a file of at most `limit` bytes as it stands, whatever it holds. | 90 | /// Reads a file of at most `limit` bytes as it stands, whatever it holds. |
| 87 | fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>>; | 91 | fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>>; |
| ... | @@ -103,6 +107,8 @@ pub trait Storage: Send + Sync { | ... | @@ -103,6 +107,8 @@ pub trait Storage: Send + Sync { |
| 103 | fn place(&self, path: &str, ancestor: [u8; 16], name: &str) -> Result<()>; | 107 | fn place(&self, path: &str, ancestor: [u8; 16], name: &str) -> Result<()>; |
| 104 | /// Publishes a transaction made on the file's current image. | 108 | /// Publishes a transaction made on the file's current image. |
| 105 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()>; | 109 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()>; |
| 110 | /// Confirms that the file still has `base`'s stamp and is durable (`onestore::confirm`). | ||
| 111 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError>; | ||
| 106 | /// Puts the file `with` in the place of the section or TOC at `path` under the coordination | 112 | /// Puts the file `with` in the place of the section or TOC at `path` under the coordination |
| 107 | /// its writers take, provided `path` still has `base`'s stamp. | 113 | /// its writers take, provided `path` still has `base`'s stamp. |
| 108 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()>; | 114 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()>; |
| ... | @@ -134,10 +140,19 @@ impl Storage for Directory { | ... | @@ -134,10 +140,19 @@ impl Storage for Directory { |
| 134 | self.0.to_string_lossy().into_owned() | 140 | self.0.to_string_lossy().into_owned() |
| 135 | } | 141 | } |
| 136 | 142 | ||
| 143 | fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 144 | use discover::Source; | ||
| 145 | discover::Local::open(&self.0)?.entries(folder, LIMITS.entries) | ||
| 146 | } | ||
| 147 | |||
| 137 | fn exists(&self, path: &str) -> bool { | 148 | fn exists(&self, path: &str) -> bool { |
| 138 | fs::metadata(self.path(path)).is_ok() | 149 | fs::metadata(self.path(path)).is_ok() |
| 139 | } | 150 | } |
| 140 | 151 | ||
| 152 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | ||
| 153 | FileRemote(self.path(path)).stamp() | ||
| 154 | } | ||
| 155 | |||
| 141 | fn read(&self, path: &str) -> Result<Vec<u8>> { | 156 | fn read(&self, path: &str) -> Result<Vec<u8>> { |
| 142 | Ok(fs::read_file(self.path(path))?) | 157 | Ok(fs::read_file(self.path(path))?) |
| 143 | } | 158 | } |
| ... | @@ -248,6 +263,10 @@ impl Storage for Directory { | ... | @@ -248,6 +263,10 @@ impl Storage for Directory { |
| 248 | Ok(fs::commit_file(transaction, self.path(path))?) | 263 | Ok(fs::commit_file(transaction, self.path(path))?) |
| 249 | } | 264 | } |
| 250 | 265 | ||
| 266 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 267 | fs::confirm_file(self.path(path), base) | ||
| 268 | } | ||
| 269 | |||
| 251 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { | 270 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { |
| 252 | Ok(fs::supersede_file(self.path(path), base, self.path(with))?) | 271 | Ok(fs::supersede_file(self.path(path), base, self.path(with))?) |
| 253 | } | 272 | } |
| ... | @@ -288,6 +307,15 @@ impl Storage for Share { | ... | @@ -288,6 +307,15 @@ impl Storage for Share { |
| 288 | self.client.location(&self.root) | 307 | self.client.location(&self.root) |
| 289 | } | 308 | } |
| 290 | 309 | ||
| 310 | fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 311 | use discover::Source; | ||
| 312 | discover::Smb::new(&self.client, &self.root)?.entries(folder, LIMITS.entries) | ||
| 313 | } | ||
| 314 | |||
| 315 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | ||
| 316 | self.client.stamp(&self.path(path)) | ||
| 317 | } | ||
| 318 | |||
| 291 | fn exists(&self, path: &str) -> bool { | 319 | fn exists(&self, path: &str) -> bool { |
| 292 | let (folder, name) = split(path); | 320 | let (folder, name) = split(path); |
| 293 | self.client | 321 | self.client |
| ... | @@ -362,6 +390,10 @@ impl Storage for Share { | ... | @@ -362,6 +390,10 @@ impl Storage for Share { |
| 362 | .commit_transaction(&self.path(path), transaction)?) | 390 | .commit_transaction(&self.path(path), transaction)?) |
| 363 | } | 391 | } |
| 364 | 392 | ||
| 393 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 394 | self.client.confirm(&self.path(path), base) | ||
| 395 | } | ||
| 396 | |||
| 365 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { | 397 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { |
| 366 | Ok(self | 398 | Ok(self |
| 367 | .client | 399 | .client |
| ... | @@ -440,6 +472,27 @@ impl Notebook { | ... | @@ -440,6 +472,27 @@ impl Notebook { |
| 440 | ) | 472 | ) |
| 441 | } | 473 | } |
| 442 | 474 | ||
| 475 | /// Opens the notebook a Live Share host serves to `guest`. Sections open through | ||
| 476 | /// `Section::resume_hosted` with the catalog's paths; while the host can't be reached, | ||
| 477 | /// the notebook opens as its folders were last listed. | ||
| 478 | #[cfg(feature = "live")] | ||
| 479 | pub fn open_hosted( | ||
| 480 | guest: Arc<crate::live::share::Guest>, | ||
| 481 | cache: impl AsRef<Path>, | ||
| 482 | ) -> Result<Self> { | ||
| 483 | let listed = listing(cache.as_ref(), &guest.location()).with_extension("entries.json"); | ||
| 484 | Self::with( | ||
| 485 | Box::new(crate::live::share::Hosted::new(guest, listed)), | ||
| 486 | None, | ||
| 487 | cache, | ||
| 488 | ) | ||
| 489 | } | ||
| 490 | |||
| 491 | /// The storage the notebook's files are in, for a Live Share host to serve. | ||
| 492 | pub fn into_storage(self) -> Box<dyn Storage> { | ||
| 493 | self.storage | ||
| 494 | } | ||
| 495 | |||
| 443 | fn with( | 496 | fn with( |
| 444 | storage: Box<dyn Storage>, | 497 | storage: Box<dyn Storage>, |
| 445 | root: Option<PathBuf>, | 498 | root: Option<PathBuf>, |
| ... | @@ -509,6 +562,11 @@ impl Notebook { | ... | @@ -509,6 +562,11 @@ impl Notebook { |
| 509 | crate::sidecar::mappings(&*self.storage) | 562 | crate::sidecar::mappings(&*self.storage) |
| 510 | } | 563 | } |
| 511 | 564 | ||
| 565 | /// The secret of the notebook's live presence room, made where it has none. | ||
| 566 | pub fn presence_room(&self) -> Result<[u8; 16]> { | ||
| 567 | crate::sidecar::room(&*self.storage) | ||
| 568 | } | ||
| 569 | |||
| 512 | /// The picture a mapping names, once its bytes match its name. | 570 | /// The picture a mapping names, once its bytes match its name. |
| 513 | pub fn tag_art_file(&self, art: &str) -> Result<Vec<u8>> { | 571 | pub fn tag_art_file(&self, art: &str) -> Result<Vec<u8>> { |
| 514 | crate::sidecar::art(&*self.storage, art) | 572 | crate::sidecar::art(&*self.storage, art) |
| ... | @@ -2023,6 +2081,22 @@ impl Section { | ... | @@ -2023,6 +2081,22 @@ impl Section { |
| 2023 | ) | 2081 | ) |
| 2024 | } | 2082 | } |
| 2025 | 2083 | ||
| 2084 | /// Resumes a replica against a section a Live Share host serves at catalog `path`. | ||
| 2085 | #[cfg(feature = "live")] | ||
| 2086 | pub fn resume_hosted( | ||
| 2087 | path: String, | ||
| 2088 | replica: Replica, | ||
| 2089 | guest: Arc<crate::live::share::Guest>, | ||
| 2090 | notify: impl Fn() + Send + 'static, | ||
| 2091 | ) -> Result<Self> { | ||
| 2092 | Self::start( | ||
| 2093 | PathBuf::from(&path), | ||
| 2094 | replica, | ||
| 2095 | move || Ok(crate::live::share::HostedRemote::new(&guest, &path)), | ||
| 2096 | notify, | ||
| 2097 | ) | ||
| 2098 | } | ||
| 2099 | |||
| 2026 | fn start<R: Remote + 'static>( | 2100 | fn start<R: Remote + 'static>( |
| 2027 | file: PathBuf, | 2101 | file: PathBuf, |
| 2028 | replica: Replica, | 2102 | replica: Replica, |
crates/notebook/src/session/tests.rs+9| ... | @@ -28,9 +28,15 @@ impl Storage for Racing { | ... | @@ -28,9 +28,15 @@ impl Storage for Racing { |
| 28 | fn location(&self) -> String { | 28 | fn location(&self) -> String { |
| 29 | self.inner.location() | 29 | self.inner.location() |
| 30 | } | 30 | } |
| 31 | fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 32 | self.inner.entries(folder) | ||
| 33 | } | ||
| 31 | fn exists(&self, path: &str) -> bool { | 34 | fn exists(&self, path: &str) -> bool { |
| 32 | self.inner.exists(path) | 35 | self.inner.exists(path) |
| 33 | } | 36 | } |
| 37 | fn stamp(&self, path: &str) -> io::Result<Stamp> { | ||
| 38 | self.inner.stamp(path) | ||
| 39 | } | ||
| 34 | fn read(&self, path: &str) -> Result<Vec<u8>> { | 40 | fn read(&self, path: &str) -> Result<Vec<u8>> { |
| 35 | self.inner.read(path) | 41 | self.inner.read(path) |
| 36 | } | 42 | } |
| ... | @@ -64,6 +70,9 @@ impl Storage for Racing { | ... | @@ -64,6 +70,9 @@ impl Storage for Racing { |
| 64 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()> { | 70 | fn commit(&self, path: &str, transaction: &Transaction) -> Result<()> { |
| 65 | self.inner.commit(path, transaction) | 71 | self.inner.commit(path, transaction) |
| 66 | } | 72 | } |
| 73 | fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> { | ||
| 74 | self.inner.confirm(path, base) | ||
| 75 | } | ||
| 67 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { | 76 | fn supersede(&self, path: &str, base: &Stamp, with: &str) -> Result<()> { |
| 68 | self.race(path); | 77 | self.race(path); |
| 69 | self.inner.supersede(path, base, with) | 78 | self.inner.supersede(path, base, with) |
crates/notebook/src/sidecar.rs+52| ... | @@ -17,6 +17,9 @@ use std::io; | ... | @@ -17,6 +17,9 @@ use std::io; |
| 17 | 17 | ||
| 18 | const FOLDER: &str = ".snowbound"; | 18 | const FOLDER: &str = ".snowbound"; |
| 19 | const MAPPING: &str = ".snowbound/tags.json"; | 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"; | ||
| 20 | const ART: &str = ".snowbound/tags"; | 23 | const ART: &str = ".snowbound/tags"; |
| 21 | /// The most bytes read of a mapping or a picture. | 24 | /// The most bytes read of a mapping or a picture. |
| 22 | pub const LIMIT: usize = 1 << 20; | 25 | pub const LIMIT: usize = 1 << 20; |
| ... | @@ -155,6 +158,55 @@ pub(crate) fn map( | ... | @@ -155,6 +158,55 @@ pub(crate) fn map( |
| 155 | Err(io::Error::from(io::ErrorKind::ResourceBusy).into()) | 158 | Err(io::Error::from(io::ErrorKind::ResourceBusy).into()) |
| 156 | } | 159 | } |
| 157 | 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 | |||
| 158 | /// A name beside `path` no other writer picks. | 210 | /// A name beside `path` no other writer picks. |
| 159 | fn temporary(path: &str) -> String { | 211 | fn temporary(path: &str) -> String { |
| 160 | use std::hash::{BuildHasher, RandomState}; | 212 | use std::hash::{BuildHasher, RandomState}; |
crates/notebook/src/sidecar/tests.rs+37| ... | @@ -83,6 +83,22 @@ impl Storage for Folder { | ... | @@ -83,6 +83,22 @@ impl Storage for Folder { |
| 83 | "memory".into() | 83 | "memory".into() |
| 84 | } | 84 | } |
| 85 | 85 | ||
| 86 | fn entries(&self, _: &str) -> io::Result<Vec<discover::Entry>> { | ||
| 87 | unimplemented!() | ||
| 88 | } | ||
| 89 | |||
| 90 | fn stamp(&self, _: &str) -> io::Result<onestore::Stamp> { | ||
| 91 | unimplemented!() | ||
| 92 | } | ||
| 93 | |||
| 94 | fn confirm( | ||
| 95 | &self, | ||
| 96 | _: &str, | ||
| 97 | _: &onestore::Stamp, | ||
| 98 | ) -> std::result::Result<(), onestore::CommitError> { | ||
| 99 | unimplemented!() | ||
| 100 | } | ||
| 101 | |||
| 86 | fn exists(&self, path: &str) -> bool { | 102 | fn exists(&self, path: &str) -> bool { |
| 87 | self.files.lock().unwrap().contains_key(path) || self.folders.lock().unwrap().contains(path) | 103 | self.files.lock().unwrap().contains_key(path) || self.folders.lock().unwrap().contains(path) |
| 88 | } | 104 | } |
| ... | @@ -341,3 +357,24 @@ fn themes_merge_into_the_hidden_folder() { | ... | @@ -341,3 +357,24 @@ fn themes_merge_into_the_hidden_folder() { |
| 341 | }; | 357 | }; |
| 342 | assert!(themes::write(&folder, built_in).is_err()); | 358 | assert!(themes::write(&folder, built_in).is_err()); |
| 343 | } | 359 | } |
| 360 | |||
| 361 | /// The presence room's secret is made once, in the hidden folder, and every reader after | ||
| 362 | /// takes it; a writer that finds one made meanwhile takes that one. | ||
| 363 | #[test] | ||
| 364 | fn the_presence_room_is_made_once() { | ||
| 365 | let folder = Folder::default(); | ||
| 366 | let secret = room(&folder).unwrap(); | ||
| 367 | assert_eq!(room(&folder).unwrap(), secret); | ||
| 368 | assert!(folder.hidden.lock().unwrap().contains(FOLDER)); | ||
| 369 | let files: Vec<_> = folder.files.lock().unwrap().keys().cloned().collect(); | ||
| 370 | assert_eq!(files, [ROOM]); | ||
| 371 | let theirs = br#"{"room":"000102030405060708090a0b0c0d0e0f"}"#; | ||
| 372 | let other = Folder::default(); | ||
| 373 | other.folders.lock().unwrap().insert(FOLDER.into()); | ||
| 374 | other | ||
| 375 | .files | ||
| 376 | .lock() | ||
| 377 | .unwrap() | ||
| 378 | .insert(ROOM.into(), theirs.to_vec()); | ||
| 379 | assert_eq!(room(&other).unwrap(), std::array::from_fn(|at| at as u8)); | ||
| 380 | } |
crates/notebook/tests/live_share.rs created+431| ... | @@ -0,0 +1,431 @@ | ||
| 1 | //! Live Share: guests open a notebook a host serves through a relay, edit it through the | ||
| 2 | //! replica and queue they use on a share, queue while the host is away, conflict as on a | ||
| 3 | //! share, pass protected sections through as ciphertext, and are refused with a wrong code. | ||
| 4 | #![cfg(feature = "live")] | ||
| 5 | |||
| 6 | use notebook::{ | ||
| 7 | EditStatus, Replica, | ||
| 8 | live::{ | ||
| 9 | Hello, | ||
| 10 | share::{self, Guest, Host, Refusal, Sharing}, | ||
| 11 | }, | ||
| 12 | session::{Notebook, Section, SyncState}, | ||
| 13 | }; | ||
| 14 | use onestore::{ | ||
| 15 | Arena, ExGuid, | ||
| 16 | op::{Edit, Op, PageOp}, | ||
| 17 | protected::{Key, rekey}, | ||
| 18 | }; | ||
| 19 | use std::{ | ||
| 20 | net::TcpListener, | ||
| 21 | path::Path, | ||
| 22 | sync::Arc, | ||
| 23 | thread, | ||
| 24 | time::{Duration, Instant}, | ||
| 25 | }; | ||
| 26 | |||
| 27 | #[path = "support/server.rs"] | ||
| 28 | mod server; | ||
| 29 | |||
| 30 | const PASSWORD: &str = "fixture password"; | ||
| 31 | |||
| 32 | /// A relay on this computer with `config`'s limits: its URL. | ||
| 33 | fn relay(config: relay::server::Config) -> String { | ||
| 34 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); | ||
| 35 | let url = format!("ws://{}", listener.local_addr().unwrap()); | ||
| 36 | thread::spawn(move || relay::server::serve(listener, config)); | ||
| 37 | url | ||
| 38 | } | ||
| 39 | |||
| 40 | fn hello(name: &str) -> Hello { | ||
| 41 | Hello::new(name.into(), None).unwrap() | ||
| 42 | } | ||
| 43 | |||
| 44 | fn until(what: &str, done: impl Fn() -> bool) { | ||
| 45 | let deadline = Instant::now() + Duration::from_secs(30); | ||
| 46 | while !done() { | ||
| 47 | assert!(Instant::now() < deadline, "{what}"); | ||
| 48 | thread::sleep(Duration::from_millis(20)); | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | /// A notebook folder holding `Garden.one`, a page reading "Original text", and a protected | ||
| 53 | /// `Sealed.one` reading "Sealed text". | ||
| 54 | fn notebook(root: &Path) -> std::path::PathBuf { | ||
| 55 | let folder = root.join("Garden"); | ||
| 56 | std::fs::create_dir_all(&folder).unwrap(); | ||
| 57 | std::fs::write( | ||
| 58 | folder.join("Garden.one"), | ||
| 59 | onestore::create_section("Garden.one", "Original text", "Fixture").unwrap(), | ||
| 60 | ) | ||
| 61 | .unwrap(); | ||
| 62 | let plain = onestore::create_section("Sealed.one", "Sealed text", "Fixture").unwrap(); | ||
| 63 | let key = Key::new(PASSWORD).unwrap(); | ||
| 64 | std::fs::write( | ||
| 65 | folder.join("Sealed.one"), | ||
| 66 | rekey(&plain, None, Some(&key)).unwrap(), | ||
| 67 | ) | ||
| 68 | .unwrap(); | ||
| 69 | folder | ||
| 70 | } | ||
| 71 | |||
| 72 | fn host(folder: &Path, cache: &Path, sharing: &Sharing, url: &str) -> Host { | ||
| 73 | let storage = Notebook::open(folder, cache).unwrap().into_storage(); | ||
| 74 | Host::start( | ||
| 75 | storage, | ||
| 76 | hello("Ada"), | ||
| 77 | sharing.clone(), | ||
| 78 | "Garden", | ||
| 79 | None, | ||
| 80 | Some(url), | ||
| 81 | || {}, | ||
| 82 | ) | ||
| 83 | .unwrap() | ||
| 84 | } | ||
| 85 | |||
| 86 | /// The host's code once the relay has numbered it. | ||
| 87 | fn code(host: &Host) -> String { | ||
| 88 | until("the code was never numbered", || { | ||
| 89 | host.code().is_some_and(|code| share::code(&code).is_some()) | ||
| 90 | }); | ||
| 91 | host.code().unwrap() | ||
| 92 | } | ||
| 93 | |||
| 94 | /// `name` joins with `code` and opens the notebook in `cache`. | ||
| 95 | fn guest(name: &str, code: &str, url: &str, cache: &Path) -> (Arc<Guest>, Notebook) { | ||
| 96 | let welcome = share::join(hello(name), code, "", None, Some(url)).unwrap(); | ||
| 97 | assert_eq!( | ||
| 98 | (welcome.notebook.as_str(), welcome.host.as_str()), | ||
| 99 | ("Garden", "Ada") | ||
| 100 | ); | ||
| 101 | let guest = Guest::start( | ||
| 102 | hello(name), | ||
| 103 | welcome.share, | ||
| 104 | welcome.secret, | ||
| 105 | None, | ||
| 106 | Some(url), | ||
| 107 | || {}, | ||
| 108 | ) | ||
| 109 | .unwrap(); | ||
| 110 | until("the host was never met", || guest.host().is_some()); | ||
| 111 | let notebook = Notebook::open_hosted(Arc::clone(&guest), cache).unwrap(); | ||
| 112 | (guest, notebook) | ||
| 113 | } | ||
| 114 | |||
| 115 | fn open(notebook: &Notebook, guest: &Arc<Guest>, path: &str, key: Option<&Key>) -> Section { | ||
| 116 | let replica = notebook.replica_path(path).unwrap(); | ||
| 117 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); | ||
| 118 | let replica = Replica::open_or_create(&replica, key, || notebook.read_section(path)).unwrap(); | ||
| 119 | Section::resume_hosted(path.into(), replica, Arc::clone(guest), || {}).unwrap() | ||
| 120 | } | ||
| 121 | |||
| 122 | fn replace(section: &Section, image: &[u8], range: std::ops::Range<u32>, with: &str) -> u64 { | ||
| 123 | let (space, text, _) = server::text(image); | ||
| 124 | replaced(section, space, text, range, with) | ||
| 125 | } | ||
| 126 | |||
| 127 | fn replaced( | ||
| 128 | section: &Section, | ||
| 129 | space: ExGuid, | ||
| 130 | text: ExGuid, | ||
| 131 | range: std::ops::Range<u32>, | ||
| 132 | with: &str, | ||
| 133 | ) -> u64 { | ||
| 134 | let op = PageOp::Text { | ||
| 135 | text, | ||
| 136 | range, | ||
| 137 | with: with.into(), | ||
| 138 | }; | ||
| 139 | let edit = Edit { | ||
| 140 | at: 134_000_000_000_000_000, | ||
| 141 | ops: vec![Op::Page { space, op }], | ||
| 142 | }; | ||
| 143 | section.replica().apply("Guest", edit).unwrap() | ||
| 144 | } | ||
| 145 | |||
| 146 | fn published(section: &Section, id: u64) { | ||
| 147 | until("the edit was never published", || { | ||
| 148 | matches!( | ||
| 149 | section.status(id).unwrap(), | ||
| 150 | Some(EditStatus::Published { .. }) | ||
| 151 | ) | ||
| 152 | }); | ||
| 153 | } | ||
| 154 | |||
| 155 | /// A guest opens a section through the host, its edit lands in the host's file, and the | ||
| 156 | /// host's own edit reaches the guest. | ||
| 157 | #[test] | ||
| 158 | fn a_guest_edits_the_host_s_notebook() { | ||
| 159 | let directory = tempfile::tempdir().unwrap(); | ||
| 160 | let folder = notebook(directory.path()); | ||
| 161 | let url = relay(Default::default()); | ||
| 162 | let sharing = Sharing::new("").unwrap(); | ||
| 163 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 164 | let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace")); | ||
| 165 | let paths: Vec<String> = notebook | ||
| 166 | .catalog() | ||
| 167 | .sections | ||
| 168 | .iter() | ||
| 169 | .map(|section| section.path.clone()) | ||
| 170 | .collect(); | ||
| 171 | assert_eq!(paths, ["Garden.one", "Sealed.one"]); | ||
| 172 | let section = open(&notebook, &guest, "Garden.one", None); | ||
| 173 | let file = folder.join("Garden.one"); | ||
| 174 | let id = replace(&section, &std::fs::read(&file).unwrap(), 0..8, "Grace's"); | ||
| 175 | published(&section, id); | ||
| 176 | assert_eq!( | ||
| 177 | server::text(&std::fs::read(&file).unwrap()).2, | ||
| 178 | "Grace's text" | ||
| 179 | ); | ||
| 180 | assert_eq!(host.guests().len(), 1); | ||
| 181 | |||
| 182 | // The host's own edit, as its app commits one. | ||
| 183 | let image = std::fs::read(&file).unwrap(); | ||
| 184 | let (space, text, _) = server::text(&image); | ||
| 185 | std::fs::write(&file, server::typed(&image, space, text, 0..7, "Ada's")).unwrap(); | ||
| 186 | host.touched(&["Garden.one".into()]); | ||
| 187 | until("the guest never saw the host's edit", || { | ||
| 188 | section | ||
| 189 | .page(space) | ||
| 190 | .is_ok_and(|page| server::page_texts(&page).contains(&"Ada's text".to_owned())) | ||
| 191 | }); | ||
| 192 | } | ||
| 193 | |||
| 194 | /// While the host is away a guest's edits wait in its replica, the notebook opens from its | ||
| 195 | /// last listing, and once the host is back the edits reach its file. | ||
| 196 | #[test] | ||
| 197 | fn a_guest_queues_while_the_host_is_away() { | ||
| 198 | let directory = tempfile::tempdir().unwrap(); | ||
| 199 | let folder = notebook(directory.path()); | ||
| 200 | let url = relay(Default::default()); | ||
| 201 | let sharing = Sharing::new("").unwrap(); | ||
| 202 | let host_cache = directory.path().join("host"); | ||
| 203 | let host = host(&folder, &host_cache, &sharing, &url); | ||
| 204 | let cache = directory.path().join("grace"); | ||
| 205 | let (guest, notebook) = guest("Grace", &code(&host), &url, &cache); | ||
| 206 | let section = open(&notebook, &guest, "Garden.one", None); | ||
| 207 | let file = folder.join("Garden.one"); | ||
| 208 | let image = std::fs::read(&file).unwrap(); | ||
| 209 | |||
| 210 | // Ada's computer goes to sleep. | ||
| 211 | drop(host); | ||
| 212 | until("the host never left", || guest.host().is_none()); | ||
| 213 | let id = replace(&section, &image, 0..8, "Offline"); | ||
| 214 | until("the section never said the host was away", || { | ||
| 215 | section | ||
| 216 | .sync_status() | ||
| 217 | .is_ok_and(|status| status.state() == SyncState::NotConnected && status.queued > 0) | ||
| 218 | }); | ||
| 219 | assert_eq!(std::fs::read(&file).unwrap(), image); | ||
| 220 | // The notebook opens from its last listing while the host is away. | ||
| 221 | let reopened = Notebook::open_hosted(Arc::clone(&guest), &cache).unwrap(); | ||
| 222 | assert_eq!(reopened.catalog().sections.len(), 2); | ||
| 223 | |||
| 224 | let host = self::host(&folder, &host_cache, &sharing, &url); | ||
| 225 | published(&section, id); | ||
| 226 | assert_eq!( | ||
| 227 | server::text(&std::fs::read(&file).unwrap()).2, | ||
| 228 | "Offline text" | ||
| 229 | ); | ||
| 230 | drop(host); | ||
| 231 | } | ||
| 232 | |||
| 233 | /// Two guests that change the same words while apart: the second to publish gets OneNote's | ||
| 234 | /// conflict page, in its replica and in the host's file. | ||
| 235 | #[test] | ||
| 236 | fn two_guests_on_one_page_conflict_as_on_a_share() { | ||
| 237 | let directory = tempfile::tempdir().unwrap(); | ||
| 238 | let folder = notebook(directory.path()); | ||
| 239 | let url = relay(Default::default()); | ||
| 240 | let sharing = Sharing::new("").unwrap(); | ||
| 241 | let host_cache = directory.path().join("host"); | ||
| 242 | let host = host(&folder, &host_cache, &sharing, &url); | ||
| 243 | let code = code(&host); | ||
| 244 | let (grace, grace_notebook) = guest("Grace", &code, &url, &directory.path().join("grace")); | ||
| 245 | let (alan, alan_notebook) = guest("Alan", &code, &url, &directory.path().join("alan")); | ||
| 246 | let graces = open(&grace_notebook, &grace, "Garden.one", None); | ||
| 247 | let alans = open(&alan_notebook, &alan, "Garden.one", None); | ||
| 248 | let file = folder.join("Garden.one"); | ||
| 249 | let image = std::fs::read(&file).unwrap(); | ||
| 250 | |||
| 251 | drop(host); | ||
| 252 | until("the host never left", || { | ||
| 253 | grace.host().is_none() && alan.host().is_none() | ||
| 254 | }); | ||
| 255 | let first = replace(&graces, &image, 0..8, "Grace's"); | ||
| 256 | let second = replace(&alans, &image, 0..8, "Alan's"); | ||
| 257 | let host = self::host(&folder, &host_cache, &sharing, &url); | ||
| 258 | published(&graces, first); | ||
| 259 | published(&alans, second); | ||
| 260 | let stored = std::fs::read(&file).unwrap(); | ||
| 261 | let conflicts = server::conflicts(&stored); | ||
| 262 | assert_eq!(conflicts.len(), 1, "one page holds a conflict page"); | ||
| 263 | let (user, kept) = &conflicts[0].1[0]; | ||
| 264 | assert_eq!(user, "Guest"); | ||
| 265 | let texts: Vec<String> = server::pages(&stored) | ||
| 266 | .iter() | ||
| 267 | .flat_map(|(_, page)| server::page_texts(page)) | ||
| 268 | .chain(kept.iter().cloned()) | ||
| 269 | .collect(); | ||
| 270 | assert!( | ||
| 271 | texts.contains(&"Grace's text".to_owned()) && texts.contains(&"Alan's text".to_owned()), | ||
| 272 | "{texts:?}" | ||
| 273 | ); | ||
| 274 | assert!(!alans.conflicts().unwrap().is_empty() || !graces.conflicts().unwrap().is_empty()); | ||
| 275 | drop(host); | ||
| 276 | } | ||
| 277 | |||
| 278 | /// A protected section's guest unlocks it with the password; the host only ever stores | ||
| 279 | /// what the guest sealed. | ||
| 280 | #[test] | ||
| 281 | fn a_protected_section_passes_through_as_ciphertext() { | ||
| 282 | let directory = tempfile::tempdir().unwrap(); | ||
| 283 | let folder = notebook(directory.path()); | ||
| 284 | let url = relay(Default::default()); | ||
| 285 | let sharing = Sharing::new("").unwrap(); | ||
| 286 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 287 | let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace")); | ||
| 288 | assert!(notebook.unlock("Sealed.one", "wrong password").is_err()); | ||
| 289 | let key = notebook.unlock("Sealed.one", PASSWORD).unwrap(); | ||
| 290 | let section = open(&notebook, &guest, "Sealed.one", Some(&key)); | ||
| 291 | let file = folder.join("Sealed.one"); | ||
| 292 | let arena = Arena::default(); | ||
| 293 | let mut unlocked = | ||
| 294 | onestore::Section::unlock(&arena, std::fs::read(&file).unwrap(), &key).unwrap(); | ||
| 295 | let (space, ..) = unlocked.pages().unwrap()[0]; | ||
| 296 | let page = unlocked.page(space).unwrap(); | ||
| 297 | let text = page | ||
| 298 | .objects | ||
| 299 | .iter() | ||
| 300 | .find_map(|object| match object { | ||
| 301 | onestore::page::PageObject::Outline(outline) => outline | ||
| 302 | .paragraphs | ||
| 303 | .iter() | ||
| 304 | .find_map(|p| p.text().map(|t| t.id)), | ||
| 305 | _ => None, | ||
| 306 | }) | ||
| 307 | .unwrap(); | ||
| 308 | let id = replaced(&section, space, text, 0..6, "Guarded"); | ||
| 309 | published(&section, id); | ||
| 310 | let stored = std::fs::read(&file).unwrap(); | ||
| 311 | let marker = "Guarded" | ||
| 312 | .encode_utf16() | ||
| 313 | .flat_map(u16::to_le_bytes) | ||
| 314 | .collect::<Vec<u8>>(); | ||
| 315 | assert!( | ||
| 316 | !stored.windows(marker.len()).any(|window| window == marker), | ||
| 317 | "the host's file holds the new text in the clear" | ||
| 318 | ); | ||
| 319 | assert!(onestore::Section::open(&Arena::default(), stored.clone()).is_err()); | ||
| 320 | let arena = Arena::default(); | ||
| 321 | let reread = onestore::Section::unlock(&arena, stored, &key).unwrap(); | ||
| 322 | let texts = server::page_texts(&reread.page(space).unwrap()); | ||
| 323 | assert!(texts.contains(&"Guarded text".to_owned()), "{texts:?}"); | ||
| 324 | } | ||
| 325 | |||
| 326 | /// A wrong code is refused without the guest learning anything; the host's code burns after | ||
| 327 | /// too many wrong tries and is replaced by new words; too many wrong codes from one network | ||
| 328 | /// lock it out; and a guest can't reach outside the notebook or presence's secret. | ||
| 329 | #[test] | ||
| 330 | fn wrong_codes_are_refused_and_counted() { | ||
| 331 | let directory = tempfile::tempdir().unwrap(); | ||
| 332 | let folder = notebook(directory.path()); | ||
| 333 | let url = relay(relay::server::Config { | ||
| 334 | burn_after: 2, | ||
| 335 | failures_per_minute: 3, | ||
| 336 | ..Default::default() | ||
| 337 | }); | ||
| 338 | let sharing = Sharing::new("").unwrap(); | ||
| 339 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 340 | let code = code(&host); | ||
| 341 | let number = code.split('-').next().unwrap(); | ||
| 342 | let wrong = format!("{number}-violet-ocelot"); | ||
| 343 | let join = |code: &str| share::join(hello("Mallory"), code, "", None, Some(&url)); | ||
| 344 | assert_eq!(join("not a code").unwrap_err(), Refusal::Malformed); | ||
| 345 | assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong); | ||
| 346 | assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong); | ||
| 347 | // The code burned, and the host shares new words, under a number of their own. | ||
| 348 | until("the code never changed", || { | ||
| 349 | host.code() | ||
| 350 | .is_some_and(|now| now != code && share::code(&now).is_some()) | ||
| 351 | }); | ||
| 352 | let fresh = host.code().unwrap(); | ||
| 353 | assert!(matches!( | ||
| 354 | join(&code).unwrap_err(), | ||
| 355 | Refusal::Expired | Refusal::NoOne | ||
| 356 | )); | ||
| 357 | let number = fresh.split('-').next().unwrap(); | ||
| 358 | assert_eq!( | ||
| 359 | join(&format!("{number}-violet-ocelot")).unwrap_err(), | ||
| 360 | Refusal::Wrong | ||
| 361 | ); | ||
| 362 | // A third wrong code in a minute locks this network out, even from the right code. | ||
| 363 | assert!(matches!( | ||
| 364 | join(&fresh).unwrap_err(), | ||
| 365 | Refusal::TooMany(Some(_)) | ||
| 366 | )); | ||
| 367 | |||
| 368 | // Joined, a guest still can't reach outside the notebook. | ||
| 369 | std::fs::create_dir_all(folder.join(".snowbound")).unwrap(); | ||
| 370 | std::fs::write(folder.join(".snowbound/live.json"), b"{}").unwrap(); | ||
| 371 | std::fs::write(directory.path().join("secret.txt"), b"secret").unwrap(); | ||
| 372 | let url = relay(Default::default()); | ||
| 373 | let host = self::host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 374 | let (_guest, notebook) = guest( | ||
| 375 | "Grace", | ||
| 376 | &self::code(&host), | ||
| 377 | &url, | ||
| 378 | &directory.path().join("g"), | ||
| 379 | ); | ||
| 380 | let storage = notebook.into_storage(); | ||
| 381 | for path in [ | ||
| 382 | "../secret.txt", | ||
| 383 | ".snowbound/live.json", | ||
| 384 | "/etc/hosts", | ||
| 385 | "a//b", | ||
| 386 | ] { | ||
| 387 | let error = storage.read_file(path, 1024).unwrap_err(); | ||
| 388 | assert!( | ||
| 389 | error.to_string().contains("Outside the notebook"), | ||
| 390 | "{path}: {error}" | ||
| 391 | ); | ||
| 392 | } | ||
| 393 | assert!(storage.rename_root("Elsewhere", &[]).is_err()); | ||
| 394 | } | ||
| 395 | |||
| 396 | /// Files larger than one message go up and come back a chunk at a time, through a relay that | ||
| 397 | /// hangs up on a peer with more than its queue waiting. | ||
| 398 | #[test] | ||
| 399 | fn large_files_travel_in_chunks() { | ||
| 400 | let directory = tempfile::tempdir().unwrap(); | ||
| 401 | let folder = notebook(directory.path()); | ||
| 402 | let url = relay(relay::server::Config { | ||
| 403 | queue: 400 << 10, | ||
| 404 | ..Default::default() | ||
| 405 | }); | ||
| 406 | let sharing = Sharing::new("").unwrap(); | ||
| 407 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 408 | let (_guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace")); | ||
| 409 | let storage = notebook.into_storage(); | ||
| 410 | let bytes: Vec<u8> = (0..3_000_000u32).map(|at| (at * 7 % 251) as u8).collect(); | ||
| 411 | storage | ||
| 412 | .create("Garden_onefiles/big.bin", &bytes) | ||
| 413 | .unwrap_err(); | ||
| 414 | std::fs::create_dir(folder.join("Garden_onefiles")).unwrap(); | ||
| 415 | storage.create("Garden_onefiles/big.bin", &bytes).unwrap(); | ||
| 416 | assert_eq!( | ||
| 417 | std::fs::read(folder.join("Garden_onefiles/big.bin")).unwrap(), | ||
| 418 | bytes | ||
| 419 | ); | ||
| 420 | assert_eq!( | ||
| 421 | storage | ||
| 422 | .read_file("Garden_onefiles/big.bin", 4 << 20) | ||
| 423 | .unwrap(), | ||
| 424 | bytes | ||
| 425 | ); | ||
| 426 | assert!( | ||
| 427 | storage | ||
| 428 | .read_file("Garden_onefiles/big.bin", 1 << 20) | ||
| 429 | .is_err() | ||
| 430 | ); | ||
| 431 | } | ||
crates/onestore/src/commit.rs+84| ... | @@ -497,6 +497,64 @@ impl Transaction { | ... | @@ -497,6 +497,64 @@ impl Transaction { |
| 497 | &self.base | 497 | &self.base |
| 498 | } | 498 | } |
| 499 | 499 | ||
| 500 | /// The transaction as bytes `from_bytes` reads back, to carry it to another machine to | ||
| 501 | /// commit: the base header and length, the new header, the appended bytes' length and | ||
| 502 | /// the bytes, then each patch's offset, length and bytes. Integers are little-endian. | ||
| 503 | pub fn to_bytes(&self) -> Vec<u8> { | ||
| 504 | let mut bytes = Vec::with_capacity(2064 + self.append.len()); | ||
| 505 | bytes.extend_from_slice(&self.base.header); | ||
| 506 | bytes.extend_from_slice(&self.base.length.to_le_bytes()); | ||
| 507 | bytes.extend_from_slice(&self.header); | ||
| 508 | bytes.extend_from_slice(&(self.append.len() as u64).to_le_bytes()); | ||
| 509 | bytes.extend_from_slice(&self.append); | ||
| 510 | for (offset, patch) in &self.patches { | ||
| 511 | bytes.extend_from_slice(&offset.to_le_bytes()); | ||
| 512 | bytes.extend_from_slice(&(patch.len() as u32).to_le_bytes()); | ||
| 513 | bytes.extend_from_slice(patch); | ||
| 514 | } | ||
| 515 | bytes | ||
| 516 | } | ||
| 517 | |||
| 518 | /// Reads `to_bytes`'s form, refusing a patch outside the base's data area, which no | ||
| 519 | /// transaction writes. | ||
| 520 | pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::Error> { | ||
| 521 | let malformed = |message| crate::Error { offset: 0, message }; | ||
| 522 | fn take<'a>(rest: &mut &'a [u8], length: usize) -> Result<&'a [u8], crate::Error> { | ||
| 523 | let (taken, after) = rest.split_at_checked(length).ok_or(crate::Error { | ||
| 524 | offset: 0, | ||
| 525 | message: "A truncated transaction", | ||
| 526 | })?; | ||
| 527 | *rest = after; | ||
| 528 | Ok(taken) | ||
| 529 | } | ||
| 530 | let rest = &mut &bytes[..]; | ||
| 531 | let base_header: [u8; 1024] = take(rest, 1024)?.try_into().expect("1024 bytes"); | ||
| 532 | let length = u64::from_le_bytes(take(rest, 8)?.try_into().expect("8 bytes")); | ||
| 533 | let header: [u8; 1024] = take(rest, 1024)?.try_into().expect("1024 bytes"); | ||
| 534 | let appended = u64::from_le_bytes(take(rest, 8)?.try_into().expect("8 bytes")); | ||
| 535 | let appended = usize::try_from(appended).map_err(|_| malformed("A huge append"))?; | ||
| 536 | let append = take(rest, appended)?.to_vec(); | ||
| 537 | let mut patches = Vec::new(); | ||
| 538 | while !rest.is_empty() { | ||
| 539 | let offset = u64::from_le_bytes(take(rest, 8)?.try_into().expect("8 bytes")); | ||
| 540 | let size = u32::from_le_bytes(take(rest, 4)?.try_into().expect("4 bytes")); | ||
| 541 | let patch = take(rest, size as usize)?; | ||
| 542 | if offset < 1024 || offset.saturating_add(u64::from(size)) > length { | ||
| 543 | return Err(malformed("A patch outside the base's data")); | ||
| 544 | } | ||
| 545 | patches.push((offset, patch.to_vec())); | ||
| 546 | } | ||
| 547 | Ok(Self { | ||
| 548 | base: Stamp { | ||
| 549 | header: base_header, | ||
| 550 | length, | ||
| 551 | }, | ||
| 552 | append, | ||
| 553 | patches, | ||
| 554 | header, | ||
| 555 | }) | ||
| 556 | } | ||
| 557 | |||
| 500 | /// The bytes a commit writes, by offset, in the order `apply` writes them; the header, | 558 | /// The bytes a commit writes, by offset, in the order `apply` writes them; the header, |
| 501 | /// last, covers bytes 0..1024. | 559 | /// last, covers bytes 0..1024. |
| 502 | pub fn writes(&self) -> impl Iterator<Item = (u64, &[u8])> { | 560 | pub fn writes(&self) -> impl Iterator<Item = (u64, &[u8])> { |
| ... | @@ -600,6 +658,32 @@ mod tests { | ... | @@ -600,6 +658,32 @@ mod tests { |
| 600 | } | 658 | } |
| 601 | } | 659 | } |
| 602 | 660 | ||
| 661 | #[test] | ||
| 662 | fn transactions_read_back_from_bytes() { | ||
| 663 | let transaction = Transaction { | ||
| 664 | base: Stamp { | ||
| 665 | header: [1; 1024], | ||
| 666 | length: 4096, | ||
| 667 | }, | ||
| 668 | append: vec![2; 300], | ||
| 669 | patches: vec![(1024, vec![3; 8]), (4000, vec![4; 96])], | ||
| 670 | header: [5; 1024], | ||
| 671 | }; | ||
| 672 | let bytes = transaction.to_bytes(); | ||
| 673 | assert_eq!(Transaction::from_bytes(&bytes).unwrap(), transaction); | ||
| 674 | assert!(Transaction::from_bytes(&bytes[..bytes.len() - 1]).is_err()); | ||
| 675 | let outside = Transaction { | ||
| 676 | patches: vec![(4000, vec![4; 97])], | ||
| 677 | ..transaction.clone() | ||
| 678 | }; | ||
| 679 | assert!(Transaction::from_bytes(&outside.to_bytes()).is_err()); | ||
| 680 | let header = Transaction { | ||
| 681 | patches: vec![(1000, vec![4; 8])], | ||
| 682 | ..transaction | ||
| 683 | }; | ||
| 684 | assert!(Transaction::from_bytes(&header.to_bytes()).is_err()); | ||
| 685 | } | ||
| 686 | |||
| 603 | #[test] | 687 | #[test] |
| 604 | fn a_read_torn_by_a_commit_is_read_again_then_refused() { | 688 | fn a_read_torn_by_a_commit_is_read_again_then_refused() { |
| 605 | let section = crate::create_section("Torn.one", "Text", "Fixture").unwrap(); | 689 | let section = crate::create_section("Torn.one", "Text", "Fixture").unwrap(); |
crates/snowbound/src/live.rs+2-2| ... | @@ -51,7 +51,7 @@ impl State { | ... | @@ -51,7 +51,7 @@ impl State { |
| 51 | None => Some(DEFAULT_RELAY.to_owned()), | 51 | None => Some(DEFAULT_RELAY.to_owned()), |
| 52 | }; | 52 | }; |
| 53 | let room = match std::env::var("SNOWBOUND_LIVE_CODE") { | 53 | let room = match std::env::var("SNOWBOUND_LIVE_CODE") { |
| 54 | Ok(code) => Some(Room::Code(code)), | 54 | Ok(code) => Some(Room::join(&code, "")), |
| 55 | Err(_) => self | 55 | Err(_) => self |
| 56 | .session | 56 | .session |
| 57 | .as_ref() | 57 | .as_ref() |
| ... | @@ -67,7 +67,7 @@ impl State { | ... | @@ -67,7 +67,7 @@ impl State { |
| 67 | .flatten(); | 67 | .flatten(); |
| 68 | let live = Hello::new(self.author.clone(), picture) | 68 | let live = Hello::new(self.author.clone(), picture) |
| 69 | .and_then(|me| { | 69 | .and_then(|me| { |
| 70 | live::Live::start(me, &room, reach, relay.as_deref(), move || { | 70 | live::Live::start(me, &room, reach, relay.as_deref(), move |_| { |
| 71 | redraw.wake_by_ref() | 71 | redraw.wake_by_ref() |
| 72 | }) | 72 | }) |
| 73 | }) | 73 | }) |