1use super::*;
2use smb2::msg::query_directory::{
3 FileInformationClass, QueryDirectoryFlags, QueryDirectoryRequest, QueryDirectoryResponse,
4};
5use std::collections::BTreeMap;
6
7mod records;
8pub use records::DirectoryEntry;
9use records::decode;
10
11impl Client {
12 /// Enumerates a share-relative directory completely or returns an error without a partial list.
13 /// An empty path selects the share root. The limit excludes `.` and `..`.
14 /// Concurrent directory changes are not an atomic snapshot; repeated names return ResourceBusy.
15 pub fn read_dir(&self, path: &str, limit: usize) -> io::Result<Vec<DirectoryEntry>> {
16 if path.contains('\0') || path.encode_utf16().count() > 32767 {
17 return Err(io::ErrorKind::InvalidInput.into());
18 }
19 let response: CreateResponse = self.request(
20 Command::Create,
21 CreateRequest {
22 requested_oplock_level: OplockLevel::None,
23 impersonation_level: ImpersonationLevel::Impersonation,
24 desired_access: FileAccessMask::new(0x80000000),
25 file_attributes: 0,
26 share_access: ShareAccess(7),
27 create_disposition: CreateDisposition::FileOpen,
28 create_options: 0x1,
29 name: smb2::encode_path(&path.replace('\\', "/")),
30 create_contexts: Vec::new(),
31 },
32 )?;
33 let file = File::new(self, &response);
34 let mut entries = BTreeMap::new();
35 loop {
36 let response: io::Result<QueryDirectoryResponse> =
37 self.request_with(Command::QueryDirectory, |connection| {
38 (
39 QueryDirectoryRequest {
40 file_information_class: FileInformationClass::FileDirectoryInformation,
41 flags: QueryDirectoryFlags(if entries.is_empty() { 1 } else { 0 }),
42 file_index: 0,
43 file_id: file.id.unwrap(),
44 output_buffer_length: connection
45 .params()
46 .expect("connected SMB session is negotiated")
47 .max_transact_size
48 .min(65536),
49 file_name: "*".into(),
50 },
51 CreditCharge(1),
52 )
53 });
54 let response = match response {
55 Ok(response) => response,
56 Err(error)
57 if matches!(
58 error.get_ref().and_then(|error| error.downcast_ref::<smb2::Error>()),
59 Some(smb2::Error::Protocol { status, command: Command::QueryDirectory })
60 if status.0 == 0x80000006 || (entries.is_empty() && status.0 == 0xc000000f)
61 ) =>
62 {
63 break;
64 }
65 Err(error) => return Err(error),
66 };
67 for entry in decode(&response.output_buffer)? {
68 if entries
69 .insert(entry.name, (entry.size, entry.modified, entry.attributes))
70 .is_some()
71 {
72 return Err(io::ErrorKind::ResourceBusy.into());
73 }
74 if entries.len()
75 - usize::from(entries.contains_key("."))
76 - usize::from(entries.contains_key(".."))
77 > limit
78 {
79 return Err(io::ErrorKind::FileTooLarge.into());
80 }
81 }
82 }
83 file.close()?;
84 Ok(entries
85 .into_iter()
86 .filter(|(name, _)| name != "." && name != "..")
87 .map(|(name, (size, modified, attributes))| DirectoryEntry {
88 name,
89 size,
90 modified,
91 attributes,
92 })
93 .collect())
94 }
95
96 /// Watches the directory tree at `path` as OneNote 2010 watches a notebook's folder: one
97 /// CHANGE_NOTIFY with WATCH_TREE, kept armed on the server, so an idle watch sends
98 /// nothing. `changed` runs on the client's runtime with each batch of changed paths,
99 /// relative to `path` and `/`-separated, or `""` when the server lost count of them, then
100 /// `Err(_)` once the watch ends with its connection or the client.
101 pub fn watch(
102 &self,
103 path: &str,
104 changed: impl FnMut(io::Result<Vec<String>>) + Send + 'static,
105 ) -> io::Result<()> {
106 if Handle::try_current().is_ok() {
107 return Err(io::ErrorKind::InvalidInput.into());
108 }
109 let mut connection = self
110 .connection
111 .lock()
112 .map_err(|_| io::ErrorKind::Other)?
113 .clone()
114 .ok_or(io::ErrorKind::NotConnected)?;
115 // Probing a connection whose only request is the watch would end OneNote's silence.
116 connection.set_keepalive(None);
117 let runtime = self.runtime.lock().map_err(|_| io::ErrorKind::Other)?;
118 let runtime = runtime.as_ref().ok_or(io::ErrorKind::NotConnected)?;
119 let path = path.replace('\\', "/");
120 let mut watcher = runtime
121 .block_on(async {
122 tokio::time::timeout(self.timeout, self.tree.watch(&mut connection, &path, true))
123 .await
124 })
125 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))?
126 .map_err(|error| match &error {
127 smb2::Error::Protocol { status, .. } if status.0 == 0xc0000034 => {
128 io::Error::new(io::ErrorKind::NotFound, error)
129 }
130 // STATUS_NOT_SUPPORTED, STATUS_INVALID_DEVICE_REQUEST
131 smb2::Error::Protocol { status, .. }
132 if matches!(status.0, 0xc00000bb | 0xc0000010) =>
133 {
134 io::Error::new(io::ErrorKind::Unsupported, error)
135 }
136 _ => super::io_error(error),
137 })?;
138 // However the task ends, the connection failing or the client retiring its runtime,
139 // `changed` hears that the watch did.
140 struct Ends<F: FnMut(io::Result<Vec<String>>)>(F);
141 impl<F: FnMut(io::Result<Vec<String>>)> Drop for Ends<F> {
142 fn drop(&mut self) {
143 (self.0)(Err(io::ErrorKind::NotConnected.into()));
144 }
145 }
146 let mut ends = Ends(changed);
147 runtime.spawn(async move {
148 loop {
149 match watcher.next_events().await {
150 Ok(events) => {
151 (ends.0)(Ok(events.into_iter().map(|event| event.filename).collect()))
152 }
153 Err(smb2::Error::Protocol { status, .. }) if status.0 == 0x0000010c => {
154 (ends.0)(Ok(vec![String::new()]))
155 }
156 Err(_) => return,
157 }
158 }
159 });
160 Ok(())
161 }
162}