1//! Blocking SMB access to OneNote sections, table-of-contents files and payloads.
2
3use onestore::{CommitError, CommitIo, CommitState};
4use smb2::{
5 Session, Tree,
6 client::connection::{CompoundOp, Connection, Frame, NegotiatedParams},
7 msg::{
8 close::{CloseRequest, CloseResponse},
9 create::{
10 CreateDisposition, CreateRequest, CreateResponse, ImpersonationLevel, ShareAccess,
11 },
12 flush::{FlushRequest, FlushResponse},
13 lock::{LockElement, LockRequest, LockResponse},
14 query_info::{InfoType, QueryInfoRequest, QueryInfoResponse},
15 read::{ReadRequest, ReadResponse},
16 set_info::{SetInfoRequest, SetInfoResponse},
17 write::{WriteRequest, WriteResponse},
18 },
19 pack::{Pack, ReadCursor, Unpack},
20 types::{
21 Command, CreditCharge, Dialect, FileId, OplockLevel,
22 flags::{Capabilities, FileAccessMask},
23 },
24};
25use std::{io, sync::Mutex, time::Duration};
26use tokio::runtime::{Handle, Runtime};
27
28mod directory;
29mod remote;
30pub use directory::DirectoryEntry;
31pub use remote::SmbRemote;
32
33/// FILE_ATTRIBUTE_HIDDEN.
34pub const HIDDEN: u32 = 0x2;
35
36#[derive(Default)]
37pub struct Credentials<'a> {
38 pub username: &'a str,
39 pub password: &'a str,
40 pub domain: &'a str,
41}
42
43/// Blocking connection with no automatic request replay or cached file contents.
44/// Call from a background thread outside a Tokio runtime.
45/// Paths are relative to the share; both `/` and `\` are separators.
46pub struct Client {
47 connection: Mutex<Option<Connection>>,
48 /// The server as `connect` was given it, which names the share's `location`.
49 address: String,
50 tree: Tree,
51 timeout: Duration,
52 runtime: Mutex<Option<Runtime>>,
53}
54
55impl Client {
56 /// The notebook folder `root` on this share, as `crate::location` names it.
57 pub(crate) fn location(&self, root: &str) -> String {
58 crate::location::smb(&self.address, &self.tree.share_name, root)
59 }
60
61 pub fn connect(
62 address: &str,
63 share: &str,
64 credentials: Credentials<'_>,
65 timeout: Duration,
66 ) -> io::Result<Self> {
67 if Handle::try_current().is_ok() || timeout.is_zero() {
68 return Err(io::ErrorKind::InvalidInput.into());
69 }
70 let runtime = tokio::runtime::Builder::new_multi_thread()
71 .worker_threads(1)
72 .enable_all()
73 .build()?;
74 let (connection, tree) = runtime
75 .block_on(async {
76 tokio::time::timeout(timeout, async {
77 let mut connection = sign_in(address, &credentials, timeout).await?;
78 let tree = Tree::connect(&mut connection, share)
79 .await
80 .map_err(io_error)?;
81 Ok::<_, io::Error>((connection, tree))
82 })
83 .await
84 })
85 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))??;
86 Ok(Self {
87 connection: Mutex::new(Some(connection)),
88 address: address.to_owned(),
89 tree,
90 timeout,
91 runtime: Mutex::new(Some(runtime)),
92 })
93 }
94
95 fn retire(&self) {
96 if let Some(connection) = self
97 .connection
98 .lock()
99 .unwrap_or_else(|error| error.into_inner())
100 .take()
101 {
102 connection.mark_dead();
103 }
104 if let Some(runtime) = self
105 .runtime
106 .lock()
107 .unwrap_or_else(|error| error.into_inner())
108 .take()
109 {
110 runtime.shutdown_background();
111 }
112 }
113
114 fn request<T: Unpack>(&self, command: Command, body: impl Pack) -> io::Result<T> {
115 self.request_with(command, |_| (body, CreditCharge(1)))
116 }
117
118 fn request_with<T: Unpack, B: Pack>(
119 &self,
120 command: Command,
121 prepare: impl FnOnce(&Connection) -> (B, CreditCharge),
122 ) -> io::Result<T> {
123 if Handle::try_current().is_ok() {
124 return Err(io::ErrorKind::InvalidInput.into());
125 }
126 let connection = self
127 .connection
128 .lock()
129 .map_err(|_| io::ErrorKind::Other)?
130 .clone()
131 .ok_or(io::ErrorKind::NotConnected)?;
132 let frame = {
133 let runtime = self.runtime.lock().map_err(|_| io::ErrorKind::Other)?;
134 let (body, charge) = prepare(&connection);
135 runtime
136 .as_ref()
137 .ok_or(io::ErrorKind::NotConnected)?
138 .block_on(async {
139 tokio::time::timeout(
140 self.timeout,
141 connection.execute_with_credits(
142 command,
143 &body,
144 Some(self.tree.tree_id),
145 charge,
146 ),
147 )
148 .await
149 })
150 };
151 let frame = frame
152 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))
153 .and_then(|result| result.map_err(io_error))
154 .inspect_err(|_| self.retire())?;
155 self.unpack(&self.body(command, frame)?)
156 }
157
158 /// Sends `requests` as one related compound request, which the server performs in order,
159 /// and returns each response body or status error. A request after a CREATE names the
160 /// file it opened as `FileId::SENTINEL`; each request fits one credit.
161 fn compound(&self, requests: &[(Command, &dyn Pack)]) -> io::Result<Vec<io::Result<Vec<u8>>>> {
162 if Handle::try_current().is_ok() {
163 return Err(io::ErrorKind::InvalidInput.into());
164 }
165 let connection = self
166 .connection
167 .lock()
168 .map_err(|_| io::ErrorKind::Other)?
169 .clone()
170 .ok_or(io::ErrorKind::NotConnected)?;
171 let operations: Vec<_> = requests
172 .iter()
173 .map(|(command, body)| CompoundOp::new(*command, *body, Some(self.tree.tree_id)))
174 .collect();
175 let frames = {
176 let runtime = self.runtime.lock().map_err(|_| io::ErrorKind::Other)?;
177 runtime
178 .as_ref()
179 .ok_or(io::ErrorKind::NotConnected)?
180 .block_on(async {
181 tokio::time::timeout(self.timeout, connection.execute_compound(&operations))
182 .await
183 })
184 };
185 let frames = frames
186 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))
187 .and_then(|result| result.map_err(io_error))
188 .inspect_err(|_| self.retire())?;
189 Ok(frames
190 .into_iter()
191 .zip(requests)
192 .map(|(frame, (command, _))| {
193 let frame = frame.map_err(io_error).inspect_err(|_| self.retire())?;
194 self.body(*command, frame)
195 })
196 .collect())
197 }
198
199 /// The body of a response to `command`, or its status as an error.
200 fn body(&self, command: Command, frame: Frame) -> io::Result<Vec<u8>> {
201 if frame.header.command != command {
202 self.retire();
203 return Err(io::ErrorKind::InvalidData.into());
204 }
205 if frame.header.status.0 != 0 {
206 let kind = match frame.header.status.0 {
207 0xc0000043 | 0xc0000054 | 0xc0000055 => io::ErrorKind::WouldBlock,
208 0xc0000011 => io::ErrorKind::UnexpectedEof,
209 0xc0000034 | 0xc000003a => io::ErrorKind::NotFound,
210 0xc0000035 => io::ErrorKind::AlreadyExists,
211 // Windows reports a delete-pending file as access denied too.
212 0xc0000022 | 0xc0000056 => io::ErrorKind::PermissionDenied,
213 0xc0000103 => io::ErrorKind::NotADirectory,
214 _ => io::ErrorKind::Other,
215 };
216 return Err(io::Error::new(
217 kind,
218 smb2::Error::Protocol {
219 status: frame.header.status,
220 command,
221 },
222 ));
223 }
224 Ok(frame.body)
225 }
226
227 fn unpack<T: Unpack>(&self, body: &[u8]) -> io::Result<T> {
228 T::unpack(&mut ReadCursor::new(body)).map_err(|error| {
229 self.retire();
230 io::Error::new(io::ErrorKind::InvalidData, error)
231 })
232 }
233
234 /// Opens as OneNote 2010 does: readers share everything, a writer denies other writers.
235 fn open(&self, path: &str, write: bool) -> io::Result<File<'_>> {
236 self.open_shared(path, write, if write { 5 } else { 7 })
237 }
238
239 fn open_shared(&self, path: &str, write: bool, sharing: u32) -> io::Result<File<'_>> {
240 self.open_with(
241 path,
242 if write { 0xc0000000 } else { 0x80000000 },
243 sharing,
244 CreateDisposition::FileOpen,
245 0x40,
246 )
247 }
248
249 fn open_with(
250 &self,
251 path: &str,
252 access: u32,
253 sharing: u32,
254 disposition: CreateDisposition,
255 options: u32,
256 ) -> io::Result<File<'_>> {
257 let response: CreateResponse = self.request(
258 Command::Create,
259 create_request(path, access, sharing, disposition, options)?,
260 )?;
261 Ok(File::new(self, &response))
262 }
263
264 /// Creates a file holding `bytes`; an existing file is an error.
265 pub fn create(&self, path: &str, bytes: &[u8]) -> io::Result<()> {
266 let mut file = self.open_with(path, 0xc0000000, 0, CreateDisposition::FileCreate, 0x40)?;
267 let mut written = 0;
268 while written < bytes.len() {
269 let count = file.write_at(written as u64, &bytes[written..])?;
270 if count == 0 {
271 return Err(io::ErrorKind::WriteZero.into());
272 }
273 written += count;
274 }
275 file.flush()?;
276 file.close()
277 }
278
279 /// Creates a directory; an existing one is an error.
280 pub fn create_directory(&self, path: &str) -> io::Result<()> {
281 self.open_with(path, 0x80000000, 7, CreateDisposition::FileCreate, 0x1)?
282 .close()
283 }
284
285 /// Renames or moves a file or directory within the share; an existing target is an
286 /// error.
287 pub fn rename(&self, from: &str, to: &str) -> io::Result<()> {
288 self.rename_over(from, to, false)
289 }
290
291 /// Renames a file over another within the share, replacing it.
292 pub fn replace(&self, from: &str, to: &str) -> io::Result<()> {
293 self.rename_over(from, to, true)
294 }
295
296 fn rename_over(&self, from: &str, to: &str, replace: bool) -> io::Result<()> {
297 if to.is_empty() || to.contains('\0') {
298 return Err(io::ErrorKind::InvalidInput.into());
299 }
300 let file = self.open_with(
301 from,
302 0x00010000 | 0x80000000,
303 7,
304 CreateDisposition::FileOpen,
305 0,
306 )?;
307 let name: Vec<u8> = to
308 .replace('/', "\\")
309 .encode_utf16()
310 .flat_map(u16::to_le_bytes)
311 .collect();
312 // FileRenameInformation: ReplaceIfExists, then reserved bytes and no root directory.
313 let mut buffer = vec![0; 16];
314 buffer[0] = u8::from(replace);
315 buffer.extend_from_slice(&u32::try_from(name.len()).unwrap().to_le_bytes());
316 buffer.extend_from_slice(&name);
317 let _: SetInfoResponse = self.request(
318 Command::SetInfo,
319 SetInfoRequest {
320 info_type: InfoType::File,
321 file_info_class: 10,
322 additional_information: 0,
323 file_id: file.id.ok_or(io::ErrorKind::InvalidInput)?,
324 buffer,
325 },
326 )?;
327 file.close()
328 }
329
330 /// Takes and lets go of OneNote 2010's writer opening and locks on the file at `path`, as a
331 /// commit does; `WouldBlock` while another writer holds them.
332 pub(crate) fn unheld(&self, path: &str) -> io::Result<()> {
333 self.open(path, true)?.coordinate(path, true, &[])?.close()
334 }
335
336 /// Gives a file or directory the hidden attribute, keeping its others, as OneNote 2010
337 /// skips a hidden folder.
338 pub(crate) fn hide(&self, path: &str) -> io::Result<()> {
339 // FILE_READ_ATTRIBUTES and FILE_WRITE_ATTRIBUTES.
340 let file = self.open_with(path, 0x180, 7, CreateDisposition::FileOpen, 0)?;
341 let file_id = file.id.ok_or(io::ErrorKind::InvalidInput)?;
342 // FileBasicInformation: four times, then the attributes; a time of 0 stays as it is.
343 let basic: QueryInfoResponse = self.request(
344 Command::QueryInfo,
345 QueryInfoRequest {
346 info_type: InfoType::File,
347 file_info_class: 4,
348 output_buffer_length: 40,
349 additional_information: 0,
350 flags: 0,
351 file_id,
352 input_buffer: Vec::new(),
353 },
354 )?;
355 let attributes = basic
356 .output_buffer
357 .get(32..36)
358 .and_then(|bytes| bytes.try_into().ok())
359 .map(u32::from_le_bytes)
360 .ok_or(io::ErrorKind::InvalidData)?;
361 // Set even where reported: Samba reports a dot name hidden without storing it so.
362 let mut buffer = vec![0; 40];
363 buffer[32..36].copy_from_slice(&(attributes | HIDDEN).to_le_bytes());
364 let _: SetInfoResponse = self.request(
365 Command::SetInfo,
366 SetInfoRequest {
367 info_type: InfoType::File,
368 file_info_class: 4,
369 additional_information: 0,
370 file_id,
371 buffer,
372 },
373 )?;
374 file.close()
375 }
376
377 /// Deletes a file or an empty directory.
378 pub fn delete(&self, path: &str) -> io::Result<()> {
379 self.open_with(path, 0x00010000, 7, CreateDisposition::FileOpen, 0x1000)?
380 .close()
381 }
382
383 /// Names a section or TOC file for its notebook (`onestore::place`) under native
384 /// writer coordination.
385 pub fn place(&self, path: &str, ancestor: [u8; 16], name: &str) -> Result<(), CommitError> {
386 self.commit(path, &[(0, 1024)], |file| {
387 onestore::place(file, ancestor, name).map_err(|error| CommitError {
388 state: CommitState::NotCommitted,
389 error,
390 })
391 })
392 }
393
394 /// Reads a bounded external payload while denying concurrent writes and deletion.
395 /// Empty files succeed; limits, sharing contention and failed close return no payload.
396 pub(crate) fn read_asset(&self, path: &str, limit: usize) -> io::Result<Vec<u8>> {
397 let mut file = self.open_shared(path, false, 1)?;
398 let mut bytes = Vec::new();
399 let mut block = [0; 65536];
400 loop {
401 let count = (limit - bytes.len()).min(block.len() - 1) + 1;
402 let read = file.read_at(
403 u64::try_from(bytes.len()).map_err(|_| io::ErrorKind::InvalidInput)?,
404 &mut block[..count],
405 )?;
406 if read == 0 {
407 break;
408 }
409 bytes.extend_from_slice(&block[..read]);
410 if bytes.len() > limit {
411 return Err(io::ErrorKind::FileTooLarge.into());
412 }
413 }
414 file.close()?;
415 Ok(bytes)
416 }
417
418 /// Reads one bounded, consistent snapshot; contention returns WouldBlock.
419 pub fn read(&self, path: &str, limit: usize) -> io::Result<Vec<u8>> {
420 self.read_with(path, |file| {
421 snapshot(|offset, output| file.read_at(offset, output), limit).map(Some)
422 })
423 }
424
425 /// Reads consistent storage, including encrypted or incomplete document graphs.
426 /// Storage validation alone does not establish edit readiness.
427 pub fn read_storage(&self, path: &str, limit: usize) -> io::Result<Vec<u8>> {
428 self.read_with(path, |file| {
429 onestore::read_storage_snapshot(|offset, output| file.read_at(offset, output), limit)
430 })
431 }
432
433 fn read_with(
434 &self,
435 path: &str,
436 snapshot: impl FnOnce(&mut File<'_>) -> io::Result<Option<Vec<u8>>>,
437 ) -> io::Result<Vec<u8>> {
438 let mut file = self
439 .open(path, false)?
440 .coordinate(path, false, &[(0, 1024)])?;
441 let result = snapshot(&mut file)
442 .and_then(|snapshot| snapshot.ok_or_else(|| io::ErrorKind::WouldBlock.into()));
443 let closed = file.close();
444 let snapshot = result?;
445 closed?;
446 Ok(snapshot)
447 }
448
449 /// Publishes a transaction using the same native writer coordination as text commits.
450 pub fn commit_transaction(
451 &self,
452 path: &str,
453 transaction: &onestore::Transaction,
454 ) -> Result<(), CommitError> {
455 self.commit(path, &checked(transaction.base()), |file| {
456 transaction.commit(file)
457 })
458 }
459
460 /// The header and length of a revision store, in one round trip, without writer
461 /// coordination or path identity checks: a change detector for polling, never a snapshot
462 /// to edit.
463 pub fn stamp(&self, path: &str) -> io::Result<onestore::Stamp> {
464 let create = create_request(path, 0x80000000, 7, CreateDisposition::FileOpen, 0x40)?;
465 let read = read_request(FileId::SENTINEL, 0, 1024);
466 let close = CloseRequest {
467 file_id: FileId::SENTINEL,
468 flags: 0,
469 };
470 let [created, read, closed]: [_; 3] = self
471 .compound(&[
472 (Command::Create, &create),
473 (Command::Read, &read),
474 (Command::Close, &close),
475 ])?
476 .try_into()
477 .map_err(|_| io::ErrorKind::InvalidData)?;
478 let created: CreateResponse = self.unpack(&created?)?;
479 if closed.is_err() {
480 self.retire();
481 }
482 let read: ReadResponse = self.unpack(&read?)?;
483 closed?;
484 let header = read
485 .data
486 .try_into()
487 .map_err(|_| io::ErrorKind::UnexpectedEof)?;
488 Ok(onestore::Stamp {
489 header,
490 length: created.end_of_file,
491 })
492 }
493
494 /// Confirms that the file still has `base`'s stamp and is durable, then refreshes its
495 /// version, under native writer coordination.
496 pub fn confirm(&self, path: &str, base: &onestore::Stamp) -> Result<(), CommitError> {
497 self.commit(path, &checked(base), |file| onestore::confirm(file, base))
498 }
499
500 /// Puts the file `with` in the place of the revision store at `path`, provided `path` still
501 /// has `base`'s stamp, as OneNote 2010's maintenance puts a file it wrote anew in place:
502 /// under writer coordination the old file goes aside, `with` takes its name, and the old
503 /// file is deleted once released. A server renames nothing over an open file.
504 pub(crate) fn supersede(
505 &self,
506 path: &str,
507 base: &onestore::Stamp,
508 with: &str,
509 ) -> Result<(), CommitError> {
510 let aside = format!("{with}.old");
511 let failed = |state| move |error| CommitError { state, error };
512 self.commit(path, &checked(base), |file| {
513 base.check(file)
514 .and_then(|()| self.rename(path, &aside))
515 .map_err(failed(CommitState::NotCommitted))?;
516 self.rename(with, path).map_err(|error| CommitError {
517 state: match self.rename(&aside, path) {
518 Ok(()) => CommitState::NotCommitted,
519 Err(_) => CommitState::Unknown,
520 },
521 error,
522 })
523 })?;
524 self.delete(&aside).map_err(failed(CommitState::Committed))
525 }
526
527 /// Opens `path` for writing under OneNote's coordination, making `reads` with the locks.
528 fn commit(
529 &self,
530 path: &str,
531 reads: &[(u64, usize)],
532 operation: impl FnOnce(&mut File<'_>) -> Result<(), CommitError>,
533 ) -> Result<(), CommitError> {
534 let mut file = self
535 .open(path, true)
536 .and_then(|file| file.coordinate(path, true, reads))
537 .map_err(|error| CommitError {
538 state: CommitState::NotCommitted,
539 error,
540 })?;
541 let result = operation(&mut file);
542 let closed = file.close();
543 result?;
544 closed.map_err(|error| CommitError {
545 state: CommitState::Committed,
546 error,
547 })
548 }
549}
550
551/// The disk shares the server at `address` offers the account, by name.
552pub fn shares(
553 address: &str,
554 credentials: Credentials<'_>,
555 timeout: Duration,
556) -> io::Result<Vec<String>> {
557 if Handle::try_current().is_ok() || timeout.is_zero() {
558 return Err(io::ErrorKind::InvalidInput.into());
559 }
560 let runtime = tokio::runtime::Builder::new_current_thread()
561 .enable_all()
562 .build()?;
563 let shares = runtime
564 .block_on(async {
565 tokio::time::timeout(timeout, async {
566 let mut connection = sign_in(address, &credentials, timeout).await?;
567 let shares = smb2::client::list_shares(&mut connection)
568 .await
569 .map_err(io_error);
570 connection.mark_dead();
571 shares
572 })
573 .await
574 })
575 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))??;
576 Ok(shares.into_iter().map(|share| share.name).collect())
577}
578
579/// `error` inside an `io::Error` whose kind says whether the server was reached: a lost or
580/// unanswered connection is `NotConnected` or `TimedOut`, and a socket's failure keeps its kind.
581fn io_error(error: smb2::Error) -> io::Error {
582 let kind = match &error {
583 smb2::Error::Io(source) => source.kind(),
584 error => match error.kind() {
585 smb2::ErrorKind::ConnectionLost => io::ErrorKind::NotConnected,
586 smb2::ErrorKind::TimedOut => io::ErrorKind::TimedOut,
587 _ => io::ErrorKind::Other,
588 },
589 };
590 io::Error::new(kind, error)
591}
592
593/// A negotiated session with the server at `address`, signed in with `credentials`.
594async fn sign_in(
595 address: &str,
596 credentials: &Credentials<'_>,
597 timeout: Duration,
598) -> io::Result<Connection> {
599 let mut connection = Connection::connect(address, timeout)
600 .await
601 .map_err(io_error)?;
602 connection.set_compression_requested(false);
603 if let Err(error) = connection.negotiate().await {
604 connection.mark_dead();
605 return Err(if speaks_only_smb1(address, timeout).await {
606 io::Error::new(io::ErrorKind::Unsupported, Refusal::Smb1)
607 } else {
608 io_error(error)
609 });
610 }
611 Session::setup(
612 &mut connection,
613 credentials.username,
614 credentials.password,
615 credentials.domain,
616 )
617 .await
618 .map_err(io_error)?;
619 Ok(connection)
620}
621
622/// Whether the server at `address` agrees to SMB1, as one that turned SMB2 away does when
623/// it speaks only SMB1.
624async fn speaks_only_smb1(address: &str, timeout: Duration) -> bool {
625 use tokio::io::{AsyncReadExt, AsyncWriteExt};
626 const DIALECT: &[u8] = b"\x02NT LM 0.12\0";
627 let mut message = Vec::with_capacity(35 + DIALECT.len());
628 message.extend_from_slice(b"\xffSMB\x72");
629 // Status, then flags (canonical paths, case-insensitive) and flags2 (NT status codes).
630 message.extend_from_slice(&[0, 0, 0, 0, 0x18, 0x01, 0x40]);
631 // PID high, signature, reserved, TID, PID, UID and MID.
632 message.extend_from_slice(&[0; 14]);
633 message.extend_from_slice(&[0xff, 0xff, 0xff, 0xfe, 0, 0, 0, 0]);
634 message.push(0);
635 message.extend_from_slice(&(DIALECT.len() as u16).to_le_bytes());
636 message.extend_from_slice(DIALECT);
637 let probe = async {
638 let mut stream = tokio::net::TcpStream::connect(address).await?;
639 stream
640 .write_all(&(message.len() as u32).to_be_bytes())
641 .await?;
642 stream.write_all(&message).await?;
643 // The frame's length, the SMB1 header, then the word count and the dialect chosen.
644 let mut reply = [0; 39];
645 stream.read_exact(&mut reply).await?;
646 io::Result::Ok(reply[4..8] == *b"\xffSMB" && reply[37..] == [0, 0])
647 };
648 matches!(tokio::time::timeout(timeout, probe).await, Ok(Ok(true)))
649}
650
651/// Why a server turned a connection or a listing away, as a person can remedy it.
652#[derive(Clone, Copy, Debug, PartialEq, Eq)]
653pub enum Refusal {
654 /// Nothing answered at the address in time.
655 Unreachable,
656 /// The server speaks only SMB1, which this client does not.
657 Smb1,
658 /// The server refused the name and password, or a guest.
659 SignIn,
660 /// The server has no share by that name.
661 NoShare,
662 /// The account may not open the share or folder.
663 Denied,
664 /// The folder is not on the share.
665 NoFolder,
666 Other,
667}
668
669impl std::fmt::Display for Refusal {
670 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671 f.write_str(match self {
672 Self::Unreachable => "The server can't be reached",
673 Self::Smb1 => "The server speaks only SMB1",
674 Self::SignIn => "The server refused the sign-in",
675 Self::NoShare => "The server has no such share",
676 Self::Denied => "Access denied",
677 Self::NoFolder => "The folder is not on the share",
678 Self::Other => "The server refused the request",
679 })
680 }
681}
682
683impl std::error::Error for Refusal {}
684
685impl Refusal {
686 /// What an error from `Client::connect`, `shares` or `Client::read_dir` means.
687 pub fn of(error: &io::Error) -> Self {
688 use smb2::ErrorKind as Smb;
689 let inner = error.get_ref();
690 if let Some(refusal) = inner.and_then(|inner| inner.downcast_ref::<Self>()) {
691 return *refusal;
692 }
693 let Some(smb) = inner.and_then(|inner| inner.downcast_ref::<smb2::Error>()) else {
694 return match error.kind() {
695 io::ErrorKind::TimedOut | io::ErrorKind::NotConnected => Self::Unreachable,
696 _ => Self::Other,
697 };
698 };
699 let tree = matches!(
700 smb,
701 smb2::Error::Protocol {
702 command: Command::TreeConnect,
703 ..
704 }
705 );
706 match smb.kind() {
707 Smb::AuthRequired | Smb::SigningRequired => Self::SignIn,
708 Smb::NotFound if tree => Self::NoShare,
709 Smb::NotFound | Smb::NotADirectory => Self::NoFolder,
710 Smb::AccessDenied => Self::Denied,
711 Smb::Io | Smb::ConnectionLost | Smb::TimedOut => Self::Unreachable,
712 _ => Self::Other,
713 }
714 }
715}
716
717impl Drop for Client {
718 fn drop(&mut self) {
719 self.retire();
720 }
721}
722
723struct File<'a> {
724 client: &'a Client,
725 id: Option<FileId>,
726 /// Writes `flush` sends ahead of its flush, in order.
727 writes: Vec<(u64, Vec<u8>)>,
728 /// Reads made under the coordination locks, each returned once to the same `read_at`.
729 prefetched: Vec<(u64, usize, Vec<u8>)>,
730}
731impl<'a> File<'a> {
732 fn new(client: &'a Client, created: &CreateResponse) -> Self {
733 Self {
734 client,
735 id: Some(created.file_id),
736 writes: Vec::new(),
737 prefetched: Vec::new(),
738 }
739 }
740
741 /// Takes OneNote 2010's coordination locks in one request (the shared reader byte, and
742 /// to write the exclusive writer byte), makes `reads` under them, then checks that the
743 /// handle is still the file at `path`: maintenance replaces a file while holding its
744 /// locks.
745 fn coordinate(mut self, path: &str, write: bool, reads: &[(u64, usize)]) -> io::Result<Self> {
746 let id = self.id.unwrap();
747 let mut locks = vec![LockElement {
748 offset: 0xfffffffb,
749 length: 1,
750 flags: 0x11,
751 }];
752 if write {
753 locks.push(LockElement {
754 offset: 0xfffffffd,
755 length: 1,
756 flags: 0x12,
757 });
758 }
759 let lock = LockRequest {
760 file_id: id,
761 lock_sequence: 0,
762 locks,
763 };
764 let [index, volume] = identity_requests(id);
765 let reading: Vec<_> = reads
766 .iter()
767 .map(|(offset, length)| read_request(id, *offset, *length))
768 .collect();
769 let mut requests: Vec<(Command, &dyn Pack)> = vec![
770 (Command::Lock, &lock),
771 (Command::QueryInfo, &index),
772 (Command::QueryInfo, &volume),
773 ];
774 requests.extend(
775 reading
776 .iter()
777 .map(|read| (Command::Read, read as &dyn Pack)),
778 );
779 let mut responses = self.client.compound(&requests)?.into_iter();
780 let mut next = || responses.next().ok_or(io::ErrorKind::InvalidData);
781 let _: LockResponse = self.client.unpack(&next()??)?;
782 let own = self.client.identity(next()?, next()?)?;
783 for (offset, length) in reads {
784 let data = match next()? {
785 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Vec::new(),
786 body => self.client.unpack::<ReadResponse>(&body?)?.data,
787 };
788 if data.len() > *length {
789 self.client.retire();
790 return Err(io::ErrorKind::InvalidData.into());
791 }
792 self.prefetched.push((*offset, *length, data));
793 }
794 let create = create_request(path, 0x80, 7, CreateDisposition::FileOpen, 0)?;
795 let [index, volume] = identity_requests(FileId::SENTINEL);
796 let close = CloseRequest {
797 file_id: FileId::SENTINEL,
798 flags: 0,
799 };
800 let [created, index, volume, closed]: [_; 4] = self
801 .client
802 .compound(&[
803 (Command::Create, &create),
804 (Command::QueryInfo, &index),
805 (Command::QueryInfo, &volume),
806 (Command::Close, &close),
807 ])?
808 .try_into()
809 .map_err(|_| io::ErrorKind::InvalidData)?;
810 created?;
811 if closed.is_err() {
812 self.client.retire();
813 }
814 let current = self.client.identity(index, volume)?;
815 closed?;
816 if own != current {
817 return Err(io::ErrorKind::ResourceBusy.into());
818 }
819 Ok(self)
820 }
821
822 #[cfg(test)]
823 fn lock(&self, offset: u64, flags: u32) -> io::Result<()> {
824 let _: LockResponse = self.client.request(
825 Command::Lock,
826 LockRequest {
827 file_id: self.id.unwrap(),
828 lock_sequence: 0,
829 locks: vec![LockElement {
830 offset,
831 length: 1,
832 flags,
833 }],
834 },
835 )?;
836 Ok(())
837 }
838
839 fn close(mut self) -> io::Result<()> {
840 self.release()
841 }
842
843 fn release(&mut self) -> io::Result<()> {
844 if let Some(file_id) = self.id.take() {
845 let result: io::Result<CloseResponse> = self
846 .client
847 .request(Command::Close, CloseRequest { file_id, flags: 0 });
848 if result.is_err() {
849 self.client.retire();
850 }
851 result?;
852 }
853 Ok(())
854 }
855}
856impl Drop for File<'_> {
857 fn drop(&mut self) {
858 let _ = self.release();
859 }
860}
861
862impl Client {
863 /// A file's volume serial number and index, from the responses to `identity_requests`.
864 fn identity(
865 &self,
866 index: io::Result<Vec<u8>>,
867 volume: io::Result<Vec<u8>>,
868 ) -> io::Result<(u64, u32)> {
869 let index: QueryInfoResponse = self.unpack(&index?)?;
870 let index = u64::from_le_bytes(
871 index
872 .output_buffer
873 .try_into()
874 .map_err(|_| io::ErrorKind::InvalidData)?,
875 );
876 if index == 0 {
877 return Err(io::ErrorKind::Unsupported.into());
878 }
879 let volume: QueryInfoResponse = self.unpack(&volume?)?;
880 let serial = volume
881 .output_buffer
882 .get(8..12)
883 .ok_or(io::ErrorKind::InvalidData)?;
884 Ok((index, u32::from_le_bytes(serial.try_into().unwrap())))
885 }
886}
887
888/// FileInternalInformation and FileFsVolumeInformation queries.
889fn identity_requests(file_id: FileId) -> [QueryInfoRequest; 2] {
890 let query = |info_type, file_info_class, output_buffer_length| QueryInfoRequest {
891 info_type,
892 file_info_class,
893 output_buffer_length,
894 additional_information: 0,
895 flags: 0,
896 file_id,
897 input_buffer: Vec::new(),
898 };
899 [
900 query(InfoType::File, 6, 8),
901 query(InfoType::Filesystem, 1, 1024),
902 ]
903}
904
905fn create_request(
906 path: &str,
907 access: u32,
908 sharing: u32,
909 disposition: CreateDisposition,
910 options: u32,
911) -> io::Result<CreateRequest> {
912 if path.is_empty() || path.contains('\0') || path.encode_utf16().count() > 32767 {
913 return Err(io::ErrorKind::InvalidInput.into());
914 }
915 Ok(CreateRequest {
916 requested_oplock_level: OplockLevel::None,
917 impersonation_level: ImpersonationLevel::Impersonation,
918 desired_access: FileAccessMask::new(access),
919 file_attributes: 0,
920 share_access: ShareAccess(sharing),
921 create_disposition: disposition,
922 create_options: options,
923 name: smb2::encode_path(&path.replace('\\', "/")),
924 create_contexts: Vec::new(),
925 })
926}
927
928fn read_request(file_id: FileId, offset: u64, length: usize) -> ReadRequest {
929 ReadRequest {
930 file_id,
931 offset,
932 length: length as u32,
933 minimum_count: 1,
934 flags: 0,
935 padding: 0,
936 channel: 0,
937 remaining_bytes: 0,
938 read_channel_info: Vec::new(),
939 }
940}
941
942fn read_size(params: &NegotiatedParams, credits: u16, requested: usize) -> usize {
943 let budget = if params.dialect != Dialect::Smb2_0_2
944 && params.capabilities.contains(Capabilities::LARGE_MTU)
945 {
946 usize::from(credits.max(1)) * 65536
947 } else {
948 65536
949 };
950 requested
951 .min(params.max_read_size as usize)
952 .min(budget)
953 .min(1024 * 1024)
954}
955
956impl CommitIo for File<'_> {
957 fn read_at(&mut self, offset: u64, output: &mut [u8]) -> io::Result<usize> {
958 if output.is_empty() {
959 return Ok(0);
960 }
961 if let Some(at) = self
962 .prefetched
963 .iter()
964 .position(|(start, length, _)| *start == offset && *length == output.len())
965 {
966 let (_, _, data) = self.prefetched.swap_remove(at);
967 output[..data.len()].copy_from_slice(&data);
968 return Ok(data.len());
969 }
970 let mut size = 0;
971 let response: io::Result<ReadResponse> =
972 self.client.request_with(Command::Read, |connection| {
973 size = read_size(
974 &connection
975 .params()
976 .expect("connected SMB session is negotiated"),
977 connection.credits(),
978 output.len(),
979 );
980 (
981 ReadRequest {
982 file_id: self.id.unwrap(),
983 offset,
984 length: size as u32,
985 minimum_count: 1,
986 flags: 0,
987 padding: 0,
988 channel: 0,
989 remaining_bytes: 0,
990 read_channel_info: Vec::new(),
991 },
992 CreditCharge(size.div_ceil(65536) as u16),
993 )
994 });
995 let response = match response {
996 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => return Ok(0),
997 result => result?,
998 };
999 if response.data.len() > size {
1000 self.client.retire();
1001 return Err(io::ErrorKind::InvalidData.into());
1002 }
1003 output[..response.data.len()].copy_from_slice(&response.data);
1004 Ok(response.data.len())
1005 }
1006 /// Queues at most one request's worth of `bytes`; `flush` sends it.
1007 fn write_at(&mut self, offset: u64, bytes: &[u8]) -> io::Result<usize> {
1008 let size = bytes.len().min(65536);
1009 if size > 0 {
1010 self.writes.push((offset, bytes[..size].to_vec()));
1011 }
1012 Ok(size)
1013 }
1014
1015 /// Sends the queued writes and then the flush in related compound requests of at most
1016 /// sixteen, awaiting each: the server performs a compound in order, and answers the flush
1017 /// once every write before it is durable.
1018 fn flush(&mut self) -> io::Result<()> {
1019 let file_id = self.id.unwrap();
1020 let writes: Vec<_> = self
1021 .writes
1022 .drain(..)
1023 .map(|(offset, data)| WriteRequest {
1024 file_id,
1025 offset,
1026 data,
1027 data_offset: 112,
1028 flags: 0,
1029 channel: 0,
1030 remaining_bytes: 0,
1031 write_channel_info_offset: 0,
1032 write_channel_info_length: 0,
1033 })
1034 .collect();
1035 let flush = FlushRequest { file_id };
1036 let mut requests: Vec<(Command, &dyn Pack)> = writes
1037 .iter()
1038 .map(|write| (Command::Write, write as &dyn Pack))
1039 .collect();
1040 requests.push((Command::Flush, &flush));
1041 let mut sizes = writes.iter().map(|write| write.data.len());
1042 for compound in requests.chunks(16) {
1043 for (response, (command, _)) in
1044 self.client.compound(compound)?.into_iter().zip(compound)
1045 {
1046 let body = response?;
1047 if *command == Command::Flush {
1048 let _: FlushResponse = self.client.unpack(&body)?;
1049 continue;
1050 }
1051 let written: WriteResponse = self.client.unpack(&body)?;
1052 let size = sizes.next().ok_or(io::ErrorKind::InvalidData)?;
1053 if written.count as usize != size {
1054 return Err(if (written.count as usize) < size {
1055 io::ErrorKind::WriteZero
1056 } else {
1057 io::ErrorKind::InvalidData
1058 }
1059 .into());
1060 }
1061 }
1062 }
1063 Ok(())
1064 }
1065}
1066
1067/// The reads `Stamp::check` makes: the header and a probe of the last byte.
1068/// `onestore::read_snapshot`, telling a file that is stably unreadable from a torn read:
1069/// storage that is consistent yet fails validation is refused, not reported as contention.
1070fn snapshot(
1071 mut read: impl FnMut(u64, &mut [u8]) -> io::Result<usize>,
1072 limit: usize,
1073) -> io::Result<Vec<u8>> {
1074 if let Some(bytes) = onestore::read_snapshot(&mut read, limit)? {
1075 return Ok(bytes);
1076 }
1077 let storage =
1078 onestore::read_storage_snapshot(&mut read, limit)?.ok_or(io::ErrorKind::WouldBlock)?;
1079 // A writer may have finished between the two reads.
1080 let at = |offset: u64, output: &mut [u8]| {
1081 let rest = storage.get(offset as usize..).unwrap_or_default();
1082 let count = rest.len().min(output.len());
1083 output[..count].copy_from_slice(&rest[..count]);
1084 Ok(count)
1085 };
1086 if let Some(bytes) = onestore::read_snapshot(at, limit)? {
1087 return Ok(bytes);
1088 }
1089 let locked =
1090 onestore::Store::parse(&storage).is_ok_and(|store| crate::discover::locked(&store));
1091 Err(if locked {
1092 io::Error::new(io::ErrorKind::Unsupported, "Password protected")
1093 } else {
1094 io::Error::new(io::ErrorKind::InvalidData, "Can't read this section")
1095 })
1096}
1097
1098fn checked(stamp: &onestore::Stamp) -> [(u64, usize); 2] {
1099 [(0, 1024), (stamp.length.saturating_sub(1), 2)]
1100}
1101
1102#[cfg(test)]
1103mod tests;