authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 18:24:10-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 19:55:46-07:00
loge01c7cf1781d73fb57bc66e80bb1b3da9cafa9d2
tree52da27f852271fb76252deef5cba466070c5e2c0
parente9e724551a1f4aec871dbe9a3ce15eb97c1fe1a8
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: Live Share serves a notebook's storage to guests who meet its host by code

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.5

15 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
533OneNote 2010 clients and Samba; it does not establish on-device execution or533OneNote 2010 clients and Samba; it does not establish on-device execution or
534physical power-loss durability.534physical power-loss durability.
535535
536## Live presence (feature `live`)536## Live presence and Live Share (feature `live`)
537537
538`live::Live::start(hello, room, reach, relay, notify)` listens on a TCP port and, with a538`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,
540connecting to the peers in the same `Room` that it finds: a notebook's identity, or a code540connecting to the peers in the same `Room` that it finds: a notebook's or a share's random
541typed on both (`Room::Code("4-violet-otter")`). With a `relay` (`wss://live.example.net`,541secret (`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 words542With a `relay` (`wss://live.example.net`, `crates/relay`) it also joins the room there and
543alone (`Room::Code("violet-otter")`) ask the relay for a number, and `code()` then has the543meets its peers through it; the end sharing a code (`Room::share`) has the relay number its
544whole code. `connect(address)` meets a peer discovery did not find. Peers meet through544words, `code()` then has the whole code, and it burns a code after too many wrong tries.
545SPAKE2 on the room's secret, then every frame is AES-256-GCM under the keys it agreed: its545`connect(address)` meets a peer discovery did not find. Peers meet through SPAKE2 on the
546number, which is also its nonce, then a message kind and a CBOR map (`live::wire`). A frame546room's secret, then every frame is AES-256-GCM under the keys it agreed: its number, which is
547lost, repeated, reordered or forged on the way fails where it lands; the connection is547also its nonce, then a message kind and a CBOR map (`live::wire`). A frame lost, repeated,
548dropped as broken, nothing from it after the fault is applied, and the ends meet again from548reordered or forged on the way fails where it lands; the connection is dropped as broken,
549scratch. A reader skips kinds and map keys it doesn't know, so later versions add both549nothing from it after the fault is applied, and the ends meet again from scratch. A reader
550freely. `set_presence` says which section, page and caret this end has (text object and550skips kinds and map keys it doesn't know, so later versions add both freely.
551UTF-16 offset, as ops address text); a connection sends only the newest. `peers()` lists551`set_presence` says which section, page and caret this end has (text object and UTF-16
552each connected peer's `Hello` (name, picture) and presence, and `notify` runs whenever that552offset, as ops address text); a connection sends only the newest. `peers()` lists each
553changes. Dropping the `Live` leaves.553connected peer's `Hello` (name, picture) and presence; `events` hears when they change, who
554was 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
556a 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)`
559serves `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
561new words, `guests()` lists who is connected, `touched(paths)` passes the host's own changes
562on, 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
564sharing 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.
554568
555## Queue measurement569## Queue measurement
556570
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}
6668
69/// Hears the catalog paths a report names.
70pub 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))]
69pub(crate) struct Reports {74pub(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 }
344350
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 first380 /// 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 }
488528
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}
501541
502#[cfg_attr(not(feature = "smb"), allow(dead_code))]542#[cfg_attr(not(any(feature = "smb", feature = "live")), allow(dead_code))]
503impl Reports {543impl 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 they2//! 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 the3//! 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 typed4//! (`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 arrive6//! 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.
89
9mod relay;10mod relay;
11pub mod share;
10pub mod wire;12pub mod wire;
11pub use wire::{Caret, Guid, Hello, Presence, Spot};13pub use wire::{Caret, Guid, Hello, Presence, Spot};
1214
13use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent, ServiceInfo};15use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent, ServiceInfo};
16use minicbor::Encode;
14use sha2::{Digest, Sha256};17use sha2::{Digest, Sha256};
15use std::{18use 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};
27use wire::{Sealer, Side, kind};30use wire::{Sealer, Side, kind};
2831
...@@ -33,37 +36,73 @@ const GONE: Duration = Duration::from_secs(45);...@@ -33,37 +36,73 @@ const GONE: Duration = Duration::from_secs(45);
33const OPENING: Duration = Duration::from_secs(5);36const OPENING: Duration = Duration::from_secs(5);
34/// The longest wait before meeting again.37/// The longest wait before meeting again.
35const PATIENCE: Duration = Duration::from_secs(30);38const 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.
40const TRIES: u32 = 5;
3641
37/// The secret peers meet through.42/// The secret peers meet through.
38#[derive(Clone, Debug, PartialEq, Eq)]43#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum Room {44pub enum Room {
40 /// Everyone with the notebook: its table of contents' file identity, which only its files45 /// 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 whose47 /// 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 a48 /// 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}
4858
49impl Room {59impl 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 }
6088
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}
68107
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}
93132
133/// What happened, as `Live::start`'s `events` hears it on a network thread.
134pub 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)]
152pub 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.
95pub struct Live {164pub struct Live {
96 shared: Arc<Shared>,165 shared: Arc<Shared>,
97 address: SocketAddr,166 address: SocketAddr,
98 daemon: Option<ServiceDaemon>,
99}167}
100168
101struct Shared {169struct 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}
122197
123struct Link {198struct 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}
129204
205/// What a connection's writer sends next.
206enum 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)]
216pub struct Line(mpsc::Sender<Out>);
217
218impl 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, or227/// 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.
132trait Pipe: Send + Sync {229trait Pipe: Send + Sync {
...@@ -185,22 +282,14 @@ impl Write for &dyn Pipe {...@@ -185,22 +282,14 @@ impl Write for &dyn Pipe {
185impl Live {282impl Live {
186 /// Starts listening as `me` in `room`, advertised and looked for where `reach` says, or283 /// 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 whenever285 /// (`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 }
259349
260 /// Where this end listens.350 /// Where this end listens.
...@@ -263,11 +353,28 @@ impl Live {...@@ -263,11 +353,28 @@ impl Live {
263 }353 }
264354
265 /// The code others type to meet this end: the one it was given, or the one the relay355 /// 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 }
270360
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 }
289396
...@@ -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}
296424
297impl Drop for Live {425impl 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}
314442
315/// Advertises `shared` as in room `tag` on `port` and connects to the peers in its room that443impl 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.
317fn 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}
376470
377impl 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 the476 /// 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 }
400493
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 it508 /// 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, as549 // 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 true624 true
503 }625 }
504626
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}
532660
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.
663fn 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
533fn hex(bytes: &[u8]) -> String {725fn 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 this3//! 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.
55
6use super::{OPENING, PATIENCE, Pipe, Shared, Side, code_parts};6use super::{Event, OPENING, PATIENCE, Pipe, Relayed as Answer, Shared, Side, code_parts};
7use ::relay::{Notice, SLOT, Verdict, ws};7use ::relay::{Notice, SLOT, Verdict, ws};
8use base64::Engine;8use base64::Engine;
9use rustls::{ClientConfig, ClientConnection, RootCertStore, pki_types::ServerName};9use 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;
2929
30type Reader = ws::Reader<BufReader<Box<dyn Read + Send>>>;30type Reader = ws::Reader<BufReader<Box<dyn Read + Send>>>;
3131
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;
33pub(super) fn join(shared: &Arc<Shared>, url: &str) -> io::Result<()> {33/// `port` is where it listens, to advertise once the relay numbers its code.
34pub(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}
4142
...@@ -87,41 +88,56 @@ impl From<io::Error> for Failure {...@@ -87,41 +88,56 @@ impl From<io::Error> for Failure {
87 }88 }
88}89}
8990
91/// Records how the relay answered, telling `shared`'s events where it changed.
92fn 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, until101/// Stays in the room at `address`, joining again whenever the connection ends, until
91/// `shared` stops.102/// `shared` stops.
92fn keep(shared: &Arc<Shared>, address: &Address) {103fn 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
13use super::{
14 Event, Hello, Line, Live, Peer, Presence, Reach, Relayed, Room,
15 wire::{self, Failure, Reply, Request, Touched, Welcome, WireEntry, WireStamp, kind},
16};
17use crate::{Error, Result, background::Reports, discover, session::Storage};
18use onestore::{CommitError, CommitState, RevisionIndex, Stamp, Store, Transaction};
19use 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.
33const CHUNK: usize = 128 << 10;
34/// The most bytes of chunks a guest has asked for and not yet been given.
35const WINDOW: usize = 512 << 10;
36/// How long a request waits for its reply.
37const TIMEOUT: Duration = Duration::from_secs(60);
38/// The largest file read or written whole.
39const LIMIT: usize = 256 << 20;
40/// Snapshots of files being read, per guest, and how long one is kept unread.
41const SNAPSHOTS: usize = 8;
42const SNAPSHOT_AGE: Duration = Duration::from_secs(120);
43/// Sections whose image a host keeps to check guests' commits on.
44const IMAGES: usize = 4;
45/// What a host says leaving as it stops sharing.
46const STOPPED: &str = "stopped";
47
48const WORDS: &str = include_str!("words.txt");
49
50/// Two random words for a code, `violet-otter`.
51pub 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.
67pub 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)]
88pub 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
97impl 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.
113pub fn location(share: &[u8; 16]) -> String {
114 format!("live://{}", super::hex(share))
115}
116
117/// Why joining failed.
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub 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.
140pub 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.
202pub 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
214impl 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.
340fn 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.
374struct 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.
386type Image = (String, Stamp, Arc<Vec<u8>>);
387/// What a guest's requests left, by guest and request.
388type 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.
392fn 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`.
399fn folder(path: &str) -> String {
400 path.rsplit_once('/')
401 .map_or(String::new(), |(folder, _)| folder.to_owned())
402}
403
404fn refused(kind: io::ErrorKind, message: &str) -> Error {
405 io::Error::new(kind, message.to_owned()).into()
406}
407
408impl 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
691fn within(folder: &str, name: &str) -> String {
692 if folder.is_empty() {
693 name.to_owned()
694 } else {
695 format!("{folder}/{name}")
696 }
697}
698
699fn 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
713fn 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
729fn state_number(state: CommitState) -> u8 {
730 match state {
731 CommitState::NotCommitted => 0,
732 CommitState::Unknown => 1,
733 CommitState::Committed => 2,
734 }
735}
736
737fn 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.
760enum 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
769impl 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.
799pub struct Guest {
800 live: Live,
801 inner: Arc<Inner>,
802}
803
804struct 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
818impl 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
1070impl 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.
1117pub struct HostedRemote {
1118 guest: Arc<Guest>,
1119 path: String,
1120}
1121
1122impl 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
1131impl 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.
1152pub 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.
1158type Listings = BTreeMap<String, Vec<(String, u8, u64, u64)>>;
1159
1160impl 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.
1167struct Source<'a> {
1168 guest: &'a Guest,
1169 kept: Listings,
1170 listed: Listings,
1171}
1172
1173impl 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
1218impl 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]
45fn peers_meet_and_follow_presence() {45fn 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() {
70fn another_code_never_meets() {70fn 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 );
117117
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"]
147fn peers_find_each_other_on_loopback() {147fn 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 {
272fn peers_meet_through_a_relay() {272fn 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);
327327
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. Asking331 // 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}
338345
...@@ -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}
2550
26/// The kinds this version reads, as `Hello::kinds` lists them.51/// The kinds this version reads, as `Hello::kinds` lists them.
27pub const KNOWN: &[u16] = &[kind::HELLO, kind::PING, kind::PRESENCE];52pub 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];
2877
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}
48100
49impl Hello {101impl 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}
125178
179/// Why a peer leaves: `left`, or `stopped` for a host that stopped sharing.
180#[derive(Clone, Debug, PartialEq, Encode, Decode)]
181#[cbor(map)]
182pub 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)]
191pub 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)]
206pub 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)]
214pub 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)]
246pub 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)]
271pub 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)]
283pub struct WireStamp {
284 #[cbor(n(0), with = "minicbor::bytes")]
285 pub header: Vec<u8>,
286 #[n(1)]
287 pub length: u64,
288}
289
290impl 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
299impl 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)]
317pub 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`.
330const 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
353pub 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
360pub 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 = self413 let sealed = self
168 .cipher414 .cipher
crates/notebook/src/live/words.txt created+1296
...@@ -0,0 +1,1296 @@
1acid
2acorn
3acre
4acts
5afar
6affix
7aged
8agent
9agile
10aging
11agony
12ahead
13aide
14aids
15aim
16ajar
17alarm
18alias
19alibi
20alien
21alike
22alive
23aloe
24aloft
25aloha
26alone
27amend
28amino
29ample
30amuse
31angel
32anger
33angle
34ankle
35apple
36april
37apron
38aqua
39area
40arena
41argue
42arise
43armed
44armor
45army
46aroma
47array
48arson
49art
50ashen
51ashes
52atlas
53atom
54attic
55audio
56avert
57avoid
58awake
59award
60awoke
61axis
62bacon
63badge
64bagel
65baggy
66baked
67baker
68balmy
69banjo
70barge
71barn
72bash
73basil
74bask
75batch
76bath
77baton
78bats
79blade
80blank
81blast
82blaze
83bleak
84blend
85bless
86blimp
87blink
88bloat
89blob
90blog
91blot
92blunt
93blurt
94blush
95boast
96boat
97body
98boil
99bok
100bolt
101boned
102boney
103bonus
104bony
105book
106booth
107boots
108boss
109botch
110both
111boxer
112breed
113bribe
114brick
115bride
116brim
117bring
118brink
119brisk
120broad
121broil
122broke
123brook
124broom
125brush
126buck
127bud
128buggy
129bulge
130bulk
131bully
132bunch
133bunny
134bunt
135bush
136bust
137busy
138buzz
139cable
140cache
141cadet
142cage
143cake
144calm
145cameo
146canal
147candy
148cane
149canon
150cape
151card
152cargo
153carol
154carry
155carve
156case
157cash
158cause
159cedar
160chain
161chair
162chant
163chaos
164charm
165chase
166cheek
167cheer
168chef
169chess
170chest
171chew
172chief
173chili
174chill
175chip
176chomp
177chop
178chow
179chuck
180chump
181chunk
182churn
183chute
184cider
185cinch
186city
187civic
188civil
189clad
190claim
191clamp
192clap
193clash
194clasp
195class
196claw
197clay
198clean
199clear
200cleat
201cleft
202clerk
203click
204cling
205clink
206clip
207cloak
208clock
209clone
210cloth
211cloud
212clump
213coach
214coast
215coat
216cod
217coil
218coke
219cola
220cold
221colt
222coma
223come
224comic
225comma
226cone
227cope
228copy
229coral
230cork
231cost
232cot
233couch
234cough
235cover
236cozy
237craft
238cramp
239crane
240crank
241crate
242crave
243crawl
244crazy
245creme
246crepe
247crept
248crib
249cried
250crisp
251crook
252crop
253cross
254crowd
255crown
256crumb
257crush
258crust
259cub
260cult
261cupid
262cure
263curl
264curry
265curse
266curve
267curvy
268cushy
269cut
270cycle
271dab
272dad
273daily
274dairy
275daisy
276dance
277dandy
278darn
279dart
280dash
281data
282date
283dawn
284deaf
285deal
286dean
287debit
288debt
289debug
290decaf
291decal
292decay
293deck
294decor
295decoy
296deed
297delay
298denim
299dense
300dent
301depth
302derby
303desk
304dial
305diary
306dice
307dig
308dill
309dime
310dimly
311diner
312dingy
313disco
314dish
315disk
316ditch
317ditzy
318dizzy
319dock
320dodge
321doing
322doll
323dome
324donor
325donut
326dose
327dot
328dove
329down
330dowry
331doze
332drab
333drama
334drank
335draw
336dress
337dried
338drift
339drill
340drive
341drone
342droop
343drove
344drown
345drum
346dry
347duck
348duct
349dude
350dug
351duke
352duo
353dusk
354dust
355duty
356dwarf
357dwell
358eagle
359early
360earth
361easel
362east
363eaten
364eats
365ebay
366ebony
367ebook
368echo
369edge
370eel
371eject
372elbow
373elder
374elf
375elk
376elm
377elope
378elude
379elves
380email
381emit
382empty
383emu
384enter
385entry
386envoy
387equal
388erase
389error
390erupt
391essay
392etch
393evade
394even
395evict
396evil
397evoke
398exact
399exit
400fable
401faced
402fact
403fade
404fall
405false
406fancy
407fang
408fax
409feast
410feed
411femur
412fence
413fend
414ferry
415fetal
416fetch
417fever
418fiber
419fifth
420fifty
421film
422filth
423final
424finch
425fit
426five
427flag
428flaky
429flame
430flap
431flask
432fled
433flick
434fling
435flint
436flip
437flirt
438float
439flock
440flop
441floss
442flyer
443foam
444foe
445fog
446foil
447folic
448folk
449food
450fool
451found
452fox
453foyer
454frail
455frame
456fray
457fresh
458fried
459frill
460frisk
461from
462front
463frost
464froth
465frown
466froze
467fruit
468gag
469gains
470gala
471game
472gap
473gas
474gave
475gear
476gecko
477geek
478gem
479genre
480gift
481gig
482gills
483given
484giver
485glad
486glass
487glide
488gloss
489glove
490glow
491glue
492goal
493going
494golf
495gong
496good
497gooey
498goofy
499gore
500gown
501grab
502grain
503grant
504grape
505graph
506grasp
507grass
508grave
509gravy
510gray
511green
512greet
513grew
514grid
515grief
516grill
517grip
518grit
519groom
520grope
521growl
522grub
523grunt
524guide
525gulf
526gulp
527gummy
528guru
529gush
530gut
531guy
532habit
533half
534halo
535halt
536happy
537harm
538hash
539hasty
540hatch
541hate
542haven
543hazel
544hazy
545heap
546heat
547heave
548hedge
549hefty
550help
551herbs
552hers
553hub
554hug
555hula
556hull
557human
558humid
559hump
560hung
561hunk
562hunt
563hurry
564hurt
565hush
566hut
567ice
568icing
569icon
570icy
571igloo
572image
573ion
574iron
575islam
576issue
577item
578ivory
579ivy
580jab
581jam
582jaws
583jazz
584jeep
585jelly
586jet
587jiffy
588job
589jog
590jolly
591jolt
592jot
593joy
594judge
595juice
596juicy
597july
598jumbo
599jump
600junky
601juror
602jury
603keep
604keg
605kept
606kick
607kilt
608king
609kite
610kitty
611kiwi
612knee
613knelt
614koala
615kung
616ladle
617lady
618lair
619lake
620lance
621land
622lapel
623large
624lash
625lasso
626last
627latch
628late
629lazy
630left
631legal
632lemon
633lend
634lens
635lent
636level
637lever
638lid
639life
640lift
641lilac
642lily
643limb
644limes
645line
646lint
647lion
648lip
649list
650lived
651liver
652lunar
653lunch
654lung
655lurch
656lure
657lurk
658lying
659lyric
660mace
661maker
662malt
663mama
664mango
665manor
666many
667map
668march
669mardi
670marry
671mash
672match
673mate
674math
675moan
676mocha
677moist
678mold
679mom
680moody
681mop
682morse
683most
684motor
685motto
686mount
687mouse
688mousy
689mouth
690move
691movie
692mower
693mud
694mug
695mulch
696mule
697mull
698mumbo
699mummy
700mural
701muse
702music
703musky
704mute
705nacho
706nag
707nail
708name
709nanny
710nap
711navy
712near
713neat
714neon
715nerd
716nest
717net
718next
719niece
720ninth
721nutty
722oak
723oasis
724oat
725ocean
726oil
727old
728olive
729omen
730onion
731only
732ooze
733opal
734open
735opera
736opt
737otter
738ouch
739ounce
740outer
741oval
742oven
743owl
744ozone
745pace
746pagan
747pager
748palm
749panda
750panic
751pants
752panty
753paper
754park
755party
756pasta
757patch
758path
759patio
760payer
761pecan
762penny
763pep
764perch
765perky
766perm
767pest
768petal
769petri
770petty
771photo
772plank
773plant
774plaza
775plead
776plot
777plow
778pluck
779plug
780plus
781poach
782pod
783poem
784poet
785pogo
786point
787poise
788poker
789polar
790polio
791polka
792polo
793pond
794pony
795poppy
796pork
797poser
798pouch
799pound
800pout
801power
802prank
803press
804print
805prior
806prism
807prize
808probe
809prong
810proof
811props
812prude
813prune
814pry
815pug
816pull
817pulp
818pulse
819puma
820punch
821punk
822pupil
823puppy
824purr
825purse
826push
827putt
828quack
829quake
830query
831quiet
832quill
833quilt
834quit
835quota
836quote
837rabid
838race
839rack
840radar
841radio
842raft
843rage
844raid
845rail
846rake
847rally
848ramp
849ranch
850range
851rank
852rant
853rash
854raven
855reach
856react
857ream
858rebel
859recap
860relax
861relay
862relic
863remix
864repay
865repel
866reply
867rerun
868reset
869rhyme
870rice
871rich
872ride
873rigid
874rigor
875rinse
876riot
877ripen
878rise
879risk
880ritzy
881rival
882river
883roast
884robe
885robin
886rock
887rogue
888roman
889romp
890rope
891rover
892royal
893ruby
894rug
895ruin
896rule
897runny
898rush
899rust
900rut
901sadly
902sage
903said
904saint
905salad
906salon
907salsa
908salt
909same
910sandy
911santa
912satin
913sauna
914saved
915savor
916sax
917say
918scale
919scam
920scan
921scare
922scarf
923scary
924scoff
925scold
926scoop
927scoot
928scope
929score
930scorn
931scout
932scowl
933scrap
934scrub
935scuba
936scuff
937sect
938sedan
939self
940send
941sepia
942serve
943set
944seven
945shack
946shade
947shady
948shaft
949shaky
950sham
951shape
952share
953sharp
954shed
955sheep
956sheet
957shelf
958shell
959shine
960shiny
961ship
962shirt
963shock
964shop
965shore
966shout
967shove
968shown
969showy
970shred
971shrug
972shun
973shush
974shut
975shy
976sift
977silk
978silly
979silo
980sip
981siren
982sixth
983size
984skate
985skew
986skid
987skier
988skies
989skip
990skirt
991skit
992sky
993slab
994slack
995slain
996slam
997slang
998slash
999slate
1000slaw
1001sled
1002sleek
1003sleep
1004sleet
1005slept
1006slice
1007slick
1008slimy
1009sling
1010slip
1011slit
1012slob
1013slot
1014slug
1015slum
1016slurp
1017slush
1018small
1019smash
1020smell
1021smile
1022smirk
1023smog
1024snack
1025snap
1026snare
1027snarl
1028sneak
1029sneer
1030sniff
1031snore
1032snort
1033snout
1034snowy
1035snub
1036snuff
1037speak
1038speed
1039spend
1040spent
1041spew
1042spied
1043spill
1044spiny
1045spoil
1046spoke
1047spoof
1048spool
1049spoon
1050sport
1051spot
1052spout
1053spray
1054spree
1055spur
1056squad
1057squat
1058squid
1059stack
1060staff
1061stage
1062stain
1063stall
1064stamp
1065stand
1066stank
1067stark
1068start
1069stash
1070state
1071stays
1072steam
1073steep
1074stem
1075step
1076stew
1077stick
1078sting
1079stir
1080stock
1081stole
1082stomp
1083stony
1084stood
1085stool
1086stoop
1087stop
1088storm
1089stout
1090stove
1091straw
1092stray
1093strut
1094stuck
1095stud
1096stuff
1097stump
1098stung
1099stunt
1100suds
1101sugar
1102sulk
1103surf
1104sushi
1105swab
1106swan
1107swarm
1108sway
1109swear
1110sweat
1111sweep
1112swell
1113swept
1114swim
1115swing
1116swipe
1117swirl
1118swoop
1119swore
1120syrup
1121tacky
1122taco
1123tag
1124take
1125tall
1126talon
1127tamer
1128tank
1129taper
1130taps
1131tarot
1132tart
1133task
1134taste
1135tasty
1136taunt
1137thank
1138thaw
1139theft
1140theme
1141thigh
1142thing
1143think
1144thong
1145thorn
1146those
1147throb
1148thud
1149thumb
1150thump
1151thus
1152tiara
1153tidal
1154tidy
1155tiger
1156tile
1157tilt
1158tint
1159tiny
1160trace
1161track
1162trade
1163train
1164trait
1165trap
1166trash
1167tray
1168treat
1169tree
1170trek
1171trend
1172trial
1173tribe
1174trick
1175trio
1176trout
1177truce
1178truck
1179trump
1180trunk
1181try
1182tug
1183tulip
1184tummy
1185turf
1186tusk
1187tutor
1188tutu
1189tux
1190tweak
1191tweet
1192twice
1193twine
1194twins
1195twirl
1196twist
1197uncle
1198uncut
1199undo
1200unify
1201union
1202unit
1203untie
1204upon
1205upper
1206urban
1207used
1208user
1209usher
1210utter
1211value
1212vapor
1213vegan
1214venue
1215verse
1216vest
1217veto
1218vice
1219video
1220view
1221viral
1222virus
1223visa
1224visor
1225vixen
1226vocal
1227voice
1228void
1229volt
1230voter
1231vowel
1232wad
1233wafer
1234wager
1235wages
1236wagon
1237wake
1238walk
1239wand
1240wasp
1241watch
1242water
1243wavy
1244wheat
1245whiff
1246whole
1247whoop
1248wick
1249widen
1250widow
1251width
1252wife
1253wifi
1254wilt
1255wimp
1256wind
1257wing
1258wink
1259wipe
1260wired
1261wiry
1262wise
1263wish
1264wispy
1265wok
1266wolf
1267womb
1268wool
1269woozy
1270word
1271work
1272worry
1273wound
1274woven
1275wrath
1276wreck
1277wrist
1278xerox
1279yahoo
1280yam
1281yard
1282year
1283yeast
1284yelp
1285yield
1286yarn
1287yodel
1288yoga
1289yoyo
1290yummy
1291zebra
1292zero
1293zesty
1294zippy
1295zone
1296zoom
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 that1//! 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.
33
4pub use crate::background::{Background, Known};4pub use crate::background::{Background, Known, Listener};
5use crate::{5use 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 coordination112 /// 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 }
136142
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 }
140151
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 }
250265
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 }
290309
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.client321 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 }
364392
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(self398 Ok(self
367 .client399 .client
...@@ -440,6 +472,27 @@ impl Notebook {...@@ -440,6 +472,27 @@ impl Notebook {
440 )472 )
441 }473 }
442474
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 }
511564
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 }
20252083
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;
1717
18const FOLDER: &str = ".snowbound";18const FOLDER: &str = ".snowbound";
19const MAPPING: &str = ".snowbound/tags.json";19const MAPPING: &str = ".snowbound/tags.json";
20/// Live presence's room secret: whoever reads the notebook's files may see who else has it
21/// open. Live Share never serves it to guests, who meet in the share's own room.
22const ROOM: &str = ".snowbound/live.json";
20const ART: &str = ".snowbound/tags";23const 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.
22pub const LIMIT: usize = 1 << 20;25pub 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}
157160
161/// The secret of the notebook's presence room (`live::Room::Notebook`), made where it has none.
162/// The first writer's stays: a writer that finds one made meanwhile takes it.
163pub(crate) fn room(storage: &dyn Storage) -> Result<[u8; 16]> {
164 #[derive(Serialize, Deserialize)]
165 struct Room {
166 room: String,
167 }
168 let read = || -> Result<Option<[u8; 16]>> {
169 let bytes = match storage.read_file(ROOM, LIMIT) {
170 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
171 return Ok(None);
172 }
173 bytes => bytes?,
174 };
175 let room: Room = serde_json::from_slice(&bytes).map_err(io::Error::from)?;
176 let secret: Option<Vec<u8>> = (0..room.room.len())
177 .step_by(2)
178 .map(|at| u8::from_str_radix(room.room.get(at..at + 2)?, 16).ok())
179 .collect();
180 Ok(Some(
181 secret
182 .and_then(|secret| secret.try_into().ok())
183 .ok_or(io::Error::from(io::ErrorKind::InvalidData))?,
184 ))
185 };
186 if let Some(secret) = read()? {
187 return Ok(secret);
188 }
189 match storage.create_directory(FOLDER) {
190 Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::AlreadyExists => {}
191 created => created?,
192 }
193 storage.hide(FOLDER)?;
194 let mut secret = [0; 16];
195 getrandom::fill(&mut secret).map_err(|_| io::Error::other("System random source failed"))?;
196 let room = Room {
197 room: secret.iter().map(|byte| format!("{byte:02x}")).collect(),
198 };
199 let written = temporary(ROOM);
200 storage.create(
201 &written,
202 &serde_json::to_vec(&room).map_err(io::Error::from)?,
203 )?;
204 if storage.rename(&written, ROOM).is_err() {
205 storage.delete(&written)?;
206 }
207 read()?.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound).into())
208}
209
158/// A name beside `path` no other writer picks.210/// A name beside `path` no other writer picks.
159fn temporary(path: &str) -> String {211fn 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 }
8585
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]
364fn 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
6use notebook::{
7 EditStatus, Replica,
8 live::{
9 Hello,
10 share::{self, Guest, Host, Refusal, Sharing},
11 },
12 session::{Notebook, Section, SyncState},
13};
14use onestore::{
15 Arena, ExGuid,
16 op::{Edit, Op, PageOp},
17 protected::{Key, rekey},
18};
19use std::{
20 net::TcpListener,
21 path::Path,
22 sync::Arc,
23 thread,
24 time::{Duration, Instant},
25};
26
27#[path = "support/server.rs"]
28mod server;
29
30const PASSWORD: &str = "fixture password";
31
32/// A relay on this computer with `config`'s limits: its URL.
33fn 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
40fn hello(name: &str) -> Hello {
41 Hello::new(name.into(), None).unwrap()
42}
43
44fn 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".
54fn 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
72fn 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.
87fn 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`.
95fn 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
115fn 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
122fn 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
127fn 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
146fn 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]
158fn 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]
197fn 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]
236fn 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]
281fn 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]
330fn 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]
399fn 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.base497 &self.base
498 }498 }
499499
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 }
602660
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(_) => self55 Err(_) => self
56 .session56 .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 })