1#![warn(clippy::pedantic)]
2
3mod error;
32
33pub use error::{Error, Result};
34use std::ffi::CString;
35use tracing::{debug, error, trace};
36use windows_sys::Win32::Foundation::{
37 ERROR_ACCESS_DENIED, ERROR_ALREADY_ASSIGNED, ERROR_BAD_DEVICE, ERROR_BAD_DEV_TYPE,
38 ERROR_BAD_NET_NAME, ERROR_BAD_PROFILE, ERROR_BAD_PROVIDER, ERROR_BAD_USERNAME, ERROR_BUSY,
39 ERROR_CANCELLED, ERROR_CANNOT_OPEN_PROFILE, ERROR_DEVICE_ALREADY_REMEMBERED,
40 ERROR_DEVICE_IN_USE, ERROR_EXTENDED_ERROR, ERROR_INVALID_ADDRESS, ERROR_INVALID_PARAMETER,
41 ERROR_INVALID_PASSWORD, ERROR_LOGON_FAILURE, ERROR_NOT_CONNECTED, ERROR_NO_NETWORK,
42 ERROR_NO_NET_OR_BAD_PATH, ERROR_OPEN_FILES, FALSE, NO_ERROR, TRUE,
43};
44use windows_sys::Win32::NetworkManagement::WNet;
45
46pub struct SmbShare {
47 share: String,
48 username: String,
49 password: String,
50 mount_on: Option<char>,
51}
52
53impl SmbShare {
54 pub fn new(
65 share: impl Into<String>,
66 username: impl Into<String>,
67 password: impl Into<String>,
68 mount_on: Option<char>,
69 ) -> Self {
70 Self {
71 share: share.into(),
72 username: username.into(),
73 password: password.into(),
74 mount_on,
75 }
76 }
77
78 pub fn connect(&self, persist: bool, interactive: bool) -> Result<()> {
108 let local_name = self
109 .mount_on
110 .map(|ln| format!("{ln}:"))
111 .map(CString::new)
112 .transpose()?;
113
114 let local_name = match local_name {
115 Some(ref cstring) => cstring.as_c_str().as_ptr() as *mut u8,
116 None => std::ptr::null_mut(),
117 };
118
119 let mut flags = 0u32;
120
121 if persist && self.mount_on.is_some() {
122 flags |= WNet::CONNECT_UPDATE_PROFILE;
123 } else {
124 flags |= WNet::CONNECT_TEMPORARY;
125 }
126
127 if interactive {
128 flags |= WNet::CONNECT_INTERACTIVE;
129 }
130
131 debug!("Connection flags: {flags:#?}");
132
133 let share = CString::new(&*self.share)?;
134 let username = CString::new(&*self.username)?;
135 let password = CString::new(&*self.password)?;
136
137 let mut netresource = WNet::NETRESOURCEA {
139 dwDisplayType: 0, dwScope: 0, dwType: WNet::RESOURCETYPE_DISK,
142 dwUsage: 0, lpLocalName: local_name,
144 lpRemoteName: share.as_c_str().as_ptr() as *mut u8,
145 lpComment: std::ptr::null_mut(), lpProvider: std::ptr::null_mut(), };
149
150 trace!("Trying to connect to {}", self.share);
151
152 let connection_result = unsafe {
154 let username = username.as_ref().as_ptr();
155 let password = password.as_ref().as_ptr();
156 WNet::WNetAddConnection2A(
157 std::ptr::from_mut::<WNet::NETRESOURCEA>(&mut netresource),
158 password.cast::<u8>(),
159 username.cast::<u8>(),
160 flags,
161 )
162 };
163
164 debug!("Connection result: {connection_result:#?}");
165
166 let connection_result = match connection_result {
167 NO_ERROR => Ok(()),
168 ERROR_ACCESS_DENIED => Err(Error::AccessDenied),
169 ERROR_ALREADY_ASSIGNED => Err(Error::AlreadyAssigned),
170 ERROR_BAD_DEV_TYPE => Err(Error::BadDevType),
171 ERROR_BAD_DEVICE => Err(Error::BadDevice),
172 ERROR_BAD_NET_NAME => Err(Error::BadNetName),
173 ERROR_BAD_PROFILE => Err(Error::BadProfile),
174 ERROR_BAD_PROVIDER => Err(Error::BadProvider),
175 ERROR_BAD_USERNAME => Err(Error::BadUsername),
176 ERROR_BUSY => Err(Error::Busy),
177 ERROR_CANCELLED => Err(Error::Cancelled),
178 ERROR_CANNOT_OPEN_PROFILE => Err(Error::CannotOpenProfile),
179 ERROR_DEVICE_ALREADY_REMEMBERED => Err(Error::DeviceAlreadyRemembered),
180 ERROR_EXTENDED_ERROR => Err(Error::ExtendedError),
181 ERROR_INVALID_ADDRESS => Err(Error::InvalidAddress),
182 ERROR_INVALID_PARAMETER => Err(Error::InvalidParameter),
183 ERROR_INVALID_PASSWORD => Err(Error::InvalidPassword),
184 ERROR_LOGON_FAILURE => Err(Error::LogonFailure),
185 ERROR_NO_NET_OR_BAD_PATH => Err(Error::NoNetOrBadPath),
186 ERROR_NO_NETWORK => Err(Error::NoNetwork),
187 _ => Err(Error::Other),
188 };
189
190 match connection_result {
191 Ok(()) => {
192 trace!("Successfully connected");
193 }
194 Err(ref e) => error!("Connection failed: {e}"),
195 };
196
197 connection_result
198 }
199
200 pub fn disconnect(&self, persist: bool, force: bool) -> Result<()> {
214 let local_name = self
215 .mount_on
216 .map(|ln| format!("{ln}:"))
217 .map(CString::new)
218 .transpose()?;
219
220 let resource_to_disconnect = local_name.unwrap_or(CString::new(&*self.share)?);
221
222 let force = if force { TRUE } else { FALSE };
223
224 let persist = if persist && self.mount_on.is_some() {
225 WNet::CONNECT_UPDATE_PROFILE
226 } else {
227 0
228 };
229
230 let disconnect_result = unsafe {
231 WNet::WNetCancelConnection2A(resource_to_disconnect.as_ptr() as *mut u8, persist, force)
232 };
233
234 debug!("Disconnect result: {disconnect_result:#?}");
235
236 let disconnect_result = match disconnect_result {
237 NO_ERROR => Ok(()),
238 ERROR_BAD_PROFILE => Err(Error::BadProfile),
239 ERROR_CANNOT_OPEN_PROFILE => Err(Error::CannotOpenProfile),
240 ERROR_DEVICE_IN_USE => Err(Error::DeviceInUse),
241 ERROR_EXTENDED_ERROR => Err(Error::ExtendedError),
242 ERROR_NOT_CONNECTED => Err(Error::NotConnected),
243 ERROR_OPEN_FILES => Err(Error::OpenFiles),
244 _ => Err(Error::Other),
245 };
246
247 match disconnect_result {
248 Ok(()) => trace!("Successfully disconnected"),
249 Err(ref e) => error!("Disconnect failed: {e}"),
250 }
251
252 disconnect_result
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 const VALID_SHARE: &str = r"PUTYOURSHARE";
263 const CORRECT_USERNAME: &str = r"PUTYOURUSER";
264 const CORRECT_PASSWORD: &str = r"PUTYOURPASS";
265
266 const WRONG_SHARE: &str = r"\\thisisnotashare.local\Share-Name";
267 const WRONG_PASSWORD: &str = r"pass";
268
269 #[test]
272 fn sad_non_interactive_does_not_prompt_and_returns_error() {
273 let share = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, WRONG_PASSWORD, None);
274 let connection = share.connect(false, false);
275 assert!(connection.is_err());
276 if let Err(e) = connection {
277 assert!(e == Error::InvalidPassword || e == Error::LogonFailure);
278 }
279 }
280
281 #[test]
282 fn sad_non_existent_share() {
283 let share = SmbShare::new(WRONG_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, None);
284 let connection = share.connect(false, false);
285 assert!(connection.is_err());
286 if let Err(e) = connection {
287 assert_eq!(e, Error::BadNetName);
288 }
289 }
290
291 #[test]
292 fn happy_mount_on_works_and_does_not_persist() {
293 let share = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, Some('s'));
294 let connection = share.connect(false, false);
295 assert!(connection.is_ok());
296 assert!(std::path::Path::new(r"s:\").is_dir());
297 let disconnect = share.disconnect(false, false);
298 assert!(disconnect.is_ok());
299 }
300
301 #[test]
302 fn happy_deviceless_works() {
303 let share = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, None);
304 let connection = share.connect(false, false);
305 assert!(connection.is_ok());
306 assert!(std::path::Path::new(VALID_SHARE).is_dir());
307 let disconnect = share.disconnect(false, false);
308 assert!(disconnect.is_ok());
309 }
310
311 #[test]
312 fn happy_deviceless_reconnecting_is_fine() {
313 let share = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, None);
314 let connection = share.connect(false, false);
315 assert!(connection.is_ok());
316 let connection = share.connect(false, false);
317 assert!(connection.is_ok());
318 assert!(std::path::Path::new(VALID_SHARE).is_dir());
319 let disconnect = share.disconnect(false, false);
320 assert!(disconnect.is_ok());
321 }
322
323 #[test]
324 fn sad_mounted_reconnecting_returns_already_assigned_error() {
325 let share = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, Some('s'));
326 let connection = share.connect(false, false);
327 assert!(connection.is_ok());
328 assert!(std::path::Path::new(r"s:\").is_dir());
329 let connection = share.connect(false, false);
330 assert!(connection.is_err());
331 if let Err(e) = connection {
332 assert_eq!(e, Error::AlreadyAssigned);
333 }
334 let disconnect = share.disconnect(false, false);
335 assert!(disconnect.is_ok());
336 }
337
338 #[test]
339 fn happy_connecting_multiple_letters_to_same_share_works() {
340 let share_one = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, Some('s'));
341 let connection1 = share_one.connect(false, false);
342 assert!(connection1.is_ok());
343 let share_two = SmbShare::new(VALID_SHARE, CORRECT_USERNAME, CORRECT_PASSWORD, Some('t'));
344 let connection2 = share_two.connect(false, false);
345 assert!(connection2.is_ok());
346 assert!(std::path::Path::new(r"s:\").is_dir());
347 assert!(std::path::Path::new(r"t:\").is_dir());
348 let share_one_disconnect = share_one.disconnect(false, false);
349 assert!(share_one_disconnect.is_ok());
350 assert!(!std::path::Path::new(r"s:\").is_dir());
351 let share_two_disconnect = share_two.disconnect(false, false);
352 assert!(share_two_disconnect.is_ok());
353 assert!(!std::path::Path::new(r"t:\").is_dir());
354 }
355}