diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c74d416..ba2eb35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,21 +5,14 @@ on: branches: [main] pull_request: -env: - CARGO_TERM_COLOR: always - jobs: test: name: Integration tests (Windows, real SMB share) runs-on: windows-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # 2026-07 - with: - toolchain: stable - name: Provision a local user and SMB share - shell: pwsh run: | $password = ConvertTo-SecureString 'Sambrs-CI-Pass-1!' -AsPlainText -Force New-LocalUser -Name 'smbtest' -Password $password -PasswordNeverExpires | Out-Null @@ -29,33 +22,22 @@ jobs: "SAMBRS_TEST_USERNAME=$env:COMPUTERNAME\smbtest" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Unit + integration tests - run: cargo test -- --include-ignored --test-threads=1 + run: cargo test && cargo test --test integration -- --test-threads=1 env: SAMBRS_TEST_SHARE: \\localhost\sambrs-test SAMBRS_TEST_PASSWORD: Sambrs-CI-Pass-1! - msrv: - name: MSRV check (Rust 1.85, Windows target) + checks: + name: MSRV, lint, and docs (Windows target) runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # 2026-07 - with: - toolchain: '1.85' - targets: x86_64-pc-windows-msvc - - name: crate (incl. tests) compiles on the declared MSRV - run: cargo check --target x86_64-pc-windows-msvc --all-targets + run: | + rustup toolchain install 1.85 --profile minimal --target x86_64-pc-windows-msvc + cargo +1.85 check --target x86_64-pc-windows-msvc --all-targets - lint: - name: Lint and docs (Windows target) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # 2026-07 - with: - toolchain: stable - targets: x86_64-pc-windows-msvc + - run: rustup target add x86_64-pc-windows-msvc - name: rustfmt run: cargo fmt --check diff --git a/Cargo.lock b/Cargo.lock index 2588239..8c9b999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,78 +41,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "zeroize" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index 5e4e3d6..79c7162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,28 +3,26 @@ name = "sambrs" version = "0.2.0" authors = ["Samuel Van der Stappen "] license = "MIT" -readme = "README.md" edition = "2024" rust-version = "1.85" repository = "https://github.com/samvdst/sambrs" categories = ["os::windows-apis", "filesystem", "api-bindings"] keywords = ["windows", "smb", "share", "network"] -description = """ -Safe, opinionated Windows SMB client operations: connect, disconnect, persist, query, and enumerate existing network shares. -""" +description = "Safe, opinionated Windows SMB client operations: connect, disconnect, persist, query, and enumerate existing network shares." # Allowlist: ship only the crate itself, not repo/CI/agent tooling. include = ["/src", "/tests", "/README.md", "/CHANGELOG.md", "/LICENSE"] +[[test]] +name = "integration" +test = false + [target.'cfg(windows)'.dependencies] tracing = { version = "0.1", default-features = false } zeroize = "1" [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.60" -features = [ - "Win32_Foundation", - "Win32_NetworkManagement_WNet", -] +version = "0.61" +features = ["Win32_NetworkManagement_WNet"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" diff --git a/README.md b/README.md index c1e9e59..b210a2b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ fn main() -> Result<(), Box> { println!("{}", std::fs::metadata(r"D:\")?.is_dir()); - target.disconnect_with(DisconnectOptions::new().forget(true))?; + target.disconnect_with(DisconnectOptions::default().forget(true))?; Ok(()) } ``` @@ -107,15 +107,15 @@ let unc = sambrs::query::get_universal_name(r"D:\folder\file.txt")?; ## Testing -The integration tests need a real share and are ignored by default. Point -them at one and include them explicitly: +The integration tests need a real share and are excluded by default. Point +them at one and run them explicitly: ```text SAMBRS_TEST_SHARE=\\server\share SAMBRS_TEST_USERNAME=DOMAIN\user SAMBRS_TEST_PASSWORD=... -cargo test -- --include-ignored --test-threads=1 +cargo test --test integration -- --test-threads=1 ``` The tests mount real drive letters and must run single-threaded. @@ -128,7 +128,7 @@ The tests mount real drive letters and must run single-threaded. | --- | --- | | `SmbShare::new(share, user, pass, Some('d'))` | `SmbTarget::new(share).credentials(user, pass).mount_on(DriveLetter::D)` | | `share.connect(persist, interactive)` | `target.connect_with(ConnectOptions::new().persist(persist))` | -| `share.disconnect(persist, force)` | `target.disconnect_with(DisconnectOptions::new().forget(persist).force(force))` | +| `share.disconnect(persist, force)` | `target.disconnect_with(DisconnectOptions::default().forget(persist).force(force))` | | `Error::CStringConversion` | `Error::InteriorNul` | The old `persist: true` disconnect argument removed persistence, hence the diff --git a/src/enumerate.rs b/src/enumerate.rs index 1e0b2bb..0ab5e9b 100644 --- a/src/enumerate.rs +++ b/src/enumerate.rs @@ -10,9 +10,8 @@ //! # Ok::<(), sambrs::Error>(()) //! ``` -use crate::error::{Error, Result, wnet_extended_error}; +use crate::error::{Error, Result, check_wnet, wnet_extended_error}; use crate::strings::{from_pwstr, len_u32, to_wide}; -use std::collections::VecDeque; use tracing::{debug, trace}; use windows_sys::Win32::Foundation::{ ERROR_EXTENDED_ERROR, ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, HANDLE, NO_ERROR, @@ -34,9 +33,8 @@ pub struct NetResource { #[derive(Debug)] pub struct Resources { handle: HANDLE, - batch: VecDeque, - /// Enumeration buffer, reused (with any growth) across [`Self::fill`] - /// batches; `u64` elements to keep it `NETRESOURCEW`-aligned. + /// Enumeration buffer, reused with any growth between resources; `u64` + /// elements keep it `NETRESOURCEW`-aligned. buf: Vec, finished: bool, } @@ -45,28 +43,15 @@ impl Iterator for Resources { type Item = Result; fn next(&mut self) -> Option { - loop { - if let Some(resource) = self.batch.pop_front() { - return Some(Ok(resource)); - } - if self.finished { - return None; - } - if let Err(e) = self.fill() { - self.finished = true; - return Some(Err(e)); - } + if self.finished { + return None; } - } -} -impl Resources { - fn fill(&mut self) -> Result<()> { // Bounded retries as a defensive measure against a misbehaving // provider that keeps demanding a bigger buffer (mirrors the cap in // `query::wide_out`). for _ in 0..4 { - let mut count = u32::MAX; // as many entries as fit + let mut count = 1u32; let mut size = len_u32(self.buf.len() * size_of::()); // SAFETY: `self.buf` outlives the call; `size` is its size in // bytes. @@ -80,48 +65,45 @@ impl Resources { }; debug!("WNetEnumResourceW returned {status} (entries={count}, bytes={size})"); match status { + NO_ERROR if count == 0 => { + trace!("zero-entry success; treating as end of enumeration"); + self.finished = true; + return None; + } NO_ERROR => { - trace!("WNetEnumResourceW returned {count} entries"); - // A zero-entry success is out of contract (the API - // reports the end via ERROR_NO_MORE_ITEMS); treat it as - // the end of the enumeration rather than re-asking a - // misbehaving provider forever. - if count == 0 { - trace!("zero-entry success; treating as end of enumeration"); - self.finished = true; - return Ok(()); - } - // SAFETY: on success the buffer starts with `count` - // NETRESOURCEW entries; the strings they point to live in - // `self.buf` and are copied before the buffer is reused. - let entries = unsafe { - std::slice::from_raw_parts( - self.buf.as_ptr().cast::(), - count as usize, - ) - }; - self.batch.extend(entries.iter().map(|raw| unsafe { + // SAFETY: on success the buffer starts with one + // NETRESOURCEW; its strings live in `self.buf` and are + // copied before the buffer is reused. + let resource = unsafe { + let raw = &*self.buf.as_ptr().cast::(); NetResource { local_name: from_pwstr(raw.lpLocalName), remote_name: from_pwstr(raw.lpRemoteName), } - })); - return Ok(()); + }; + return Some(Ok(resource)); } ERROR_NO_MORE_ITEMS => { self.finished = true; - return Ok(()); + return None; } // Buffer too small for a single entry; `size` holds the // required size in bytes. ERROR_MORE_DATA => { self.buf = vec![0u64; (size as usize).div_ceil(size_of::())]; } - ERROR_EXTENDED_ERROR => return Err(wnet_extended_error()), - code => return Err(Error::Windows(code)), + ERROR_EXTENDED_ERROR => { + self.finished = true; + return Some(Err(wnet_extended_error())); + } + code => { + self.finished = true; + return Some(Err(Error::Windows(code))); + } } } - Err(Error::Windows(ERROR_MORE_DATA)) + self.finished = true; + Some(Err(Error::Windows(ERROR_MORE_DATA))) } } @@ -136,14 +118,9 @@ impl Drop for Resources { fn open(scope: u32, root_remote: Option<&str>) -> Result { let remote = root_remote.map(to_wide).transpose()?; let root = remote.as_ref().map(|remote| WNet::NETRESOURCEW { - dwScope: 0, - dwType: 0, - dwDisplayType: 0, dwUsage: WNet::RESOURCEUSAGE_CONTAINER, - lpLocalName: std::ptr::null_mut(), lpRemoteName: remote.as_ptr().cast_mut(), - lpComment: std::ptr::null_mut(), - lpProvider: std::ptr::null_mut(), + ..Default::default() }); let mut handle: HANDLE = std::ptr::null_mut(); @@ -158,16 +135,12 @@ fn open(scope: u32, root_remote: Option<&str>) -> Result { ) }; debug!("WNetOpenEnumW returned {status}"); - match status { - NO_ERROR => Ok(Resources { - handle, - batch: VecDeque::new(), - buf: vec![0u64; 2048], // 16 KiB to start; grows on demand - finished: false, - }), - ERROR_EXTENDED_ERROR => Err(wnet_extended_error()), - code => Err(Error::Windows(code)), - } + check_wnet(status)?; + Ok(Resources { + handle, + buf: vec![0u64; 2048], // 16 KiB to start; grows on demand + finished: false, + }) } /// Currently connected resources (`RESOURCE_CONNECTED`) — every active diff --git a/src/options.rs b/src/options.rs index f4a893a..5dcf5a3 100644 --- a/src/options.rs +++ b/src/options.rs @@ -20,14 +20,6 @@ pub enum DriveLetter { N, O, P, Q, R, S, T, U, V, W, X, Y, Z, } -impl DriveLetter { - /// The letter as an uppercase `char`. - #[must_use] - pub const fn as_char(self) -> char { - (b'A' + self as u8) as char - } -} - impl TryFrom for DriveLetter { type Error = Error; @@ -54,7 +46,7 @@ impl TryFrom for DriveLetter { impl std::fmt::Display for DriveLetter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:", self.as_char()) + write!(f, "{}:", char::from(b'A' + *self as u8)) } } @@ -122,10 +114,6 @@ pub struct DisconnectOptions { } impl DisconnectOptions { - pub fn new() -> Self { - Self::default() - } - /// Disconnect even if files or jobs remain open on the mapping. pub fn force(mut self, yes: bool) -> Self { self.force = yes; @@ -193,12 +181,4 @@ mod tests { | WNet::CONNECT_REQUIRE_PRIVACY ); } - - #[test] - fn disconnect_options_map() { - assert!(!DisconnectOptions::new().forget); - assert!(!DisconnectOptions::new().force); - assert!(DisconnectOptions::new().forget(true).forget); - assert!(DisconnectOptions::new().force(true).force); - } } diff --git a/src/strings.rs b/src/strings.rs index f7fd66b..63915ee 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -29,8 +29,8 @@ pub(crate) type WideSecret = zeroize::Zeroizing>; /// /// The caller must keep the owning buffer alive for as long as the returned /// pointer is in use. -pub(crate) fn opt_ptr(buf: Option<&[u16]>) -> *mut u16 { - buf.map_or(std::ptr::null_mut(), |b| b.as_ptr().cast_mut()) +pub(crate) fn opt_ptr(buf: Option<&[u16]>) -> *const u16 { + buf.map_or(std::ptr::null(), <[u16]>::as_ptr) } /// Owned `String` from a wide buffer, up to the first nul (or the full buffer @@ -60,14 +60,6 @@ pub(crate) unsafe fn from_pwstr(ptr: *const u16) -> Option { } } -/// Pointer to an optional secret wide string, or null when absent. -/// -/// The caller must keep the owning buffer alive for as long as the returned -/// pointer is in use. -pub(crate) fn secret_ptr(buf: Option<&WideSecret>) -> *const u16 { - buf.map_or(std::ptr::null(), |b| b.as_ptr()) -} - /// Buffer length as `u32` for Windows APIs; saturates instead of panicking. pub(crate) fn len_u32(len: usize) -> u32 { u32::try_from(len).unwrap_or(u32::MAX) @@ -78,18 +70,9 @@ mod tests { use super::*; #[test] - fn to_wide_appends_nul() { + fn to_wide_converts_and_validates() { assert_eq!(to_wide("ab").unwrap(), vec![97, 98, 0]); - } - - #[test] - fn to_wide_handles_non_ascii() { - // 'ü' is a single UTF-16 code unit but two UTF-8 bytes. assert_eq!(to_wide("ü").unwrap(), vec![0xFC, 0]); - } - - #[test] - fn to_wide_rejects_interior_nul() { assert_eq!(to_wide("a\0b").unwrap_err(), Error::InteriorNul); } diff --git a/src/target.rs b/src/target.rs index 99d8e59..c4cc179 100644 --- a/src/target.rs +++ b/src/target.rs @@ -1,6 +1,6 @@ use crate::error::{Error, Result, check_wnet}; use crate::options::{ConnectOptions, DisconnectOptions, DriveLetter}; -use crate::strings::{WideSecret, from_wide_buf, len_u32, opt_ptr, secret_ptr, to_wide}; +use crate::strings::{WideSecret, from_wide_buf, len_u32, opt_ptr, to_wide}; use tracing::{debug, trace}; use windows_sys::Win32::NetworkManagement::WNet; @@ -39,7 +39,7 @@ impl ConnectArgs { fn resource(&self) -> WNet::NETRESOURCEW { WNet::NETRESOURCEW { dwType: WNet::RESOURCETYPE_DISK, - lpLocalName: opt_ptr(self.local.as_deref()), + lpLocalName: opt_ptr(self.local.as_deref()).cast_mut(), lpRemoteName: self.remote.as_ptr().cast_mut(), ..Default::default() } @@ -128,12 +128,6 @@ impl SmbTarget { self } - /// The remote name, e.g. `\\server\share`. - #[must_use] - pub fn remote(&self) -> &str { - &self.remote - } - /// Connect with default options: a temporary, non-interactive connection. /// /// Connecting multiple times works fine in deviceless mode but fails with @@ -173,7 +167,7 @@ impl SmbTarget { let status = unsafe { WNet::WNetAddConnection2W( &raw const resource, - secret_ptr(args.password.as_ref()), + opt_ptr(args.password.as_ref().map(|password| password.as_slice())), opt_ptr(args.username.as_deref()), flags, ) @@ -222,7 +216,7 @@ impl SmbTarget { WNet::WNetUseConnectionW( std::ptr::null_mut(), // no owner window for credential dialogs &raw const resource, - secret_ptr(args.password.as_ref()), + opt_ptr(args.password.as_ref().map(|password| password.as_slice())), opt_ptr(args.username.as_deref()), flags, access_name.as_mut_ptr(), @@ -257,10 +251,7 @@ impl SmbTarget { if options.is_persistent() { return Err(Error::PersistentGuard); } - let Some(device) = self.local.as_deref() else { - return Err(Error::GuardRequiresDrive); - }; - let device = device.to_string(); + let device = self.local.clone().ok_or(Error::GuardRequiresDrive)?; self.connect_with(options)?; Ok(Connection { device, @@ -298,7 +289,7 @@ impl SmbTarget { /// # Errors /// Returns [`Error`] when the Windows call fails. pub fn disconnect(&self) -> Result<()> { - self.disconnect_with(DisconnectOptions::new()) + self.disconnect_with(DisconnectOptions::default()) } /// Disconnect with explicit [`DisconnectOptions`]. @@ -346,8 +337,7 @@ pub fn cancel_connection(name: &str, options: DisconnectOptions) -> Result<()> { } fn is_drive(name: &str) -> bool { - let bytes = name.as_bytes(); - bytes.len() == 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' + matches!(name.as_bytes(), [letter, b':'] if letter.is_ascii_alphabetic()) } /// RAII guard returned by [`SmbTarget::connect_guarded`] and @@ -392,7 +382,7 @@ impl Connection { impl Drop for Connection { fn drop(&mut self) { if self.armed { - if let Err(e) = cancel_connection(&self.device, DisconnectOptions::new()) { + if let Err(e) = cancel_connection(&self.device, DisconnectOptions::default()) { debug!("failed to disconnect {} on guard drop: {e}", self.device); } } @@ -404,14 +394,8 @@ mod tests { use super::*; #[test] - fn configures_target_fluently() { - let target = SmbTarget::new(r"\\server\share") - .username("user") - .password("secret-value") - .mount_on(DriveLetter::D); - assert_eq!(target.username.as_deref(), Some("user")); - assert_eq!(target.password.as_deref().unwrap().as_str(), "secret-value"); - assert_eq!(target.local.as_deref(), Some("D:")); + fn debug_redacts_password() { + let target = SmbTarget::new(r"\\server\share").credentials("user", "secret-value"); let debug = format!("{target:?}"); assert!(debug.contains("user")); assert!(!debug.contains("secret-value")); @@ -460,7 +444,7 @@ mod tests { Error::PersistentGuard ); assert_eq!( - deviceless.disconnect_with(DisconnectOptions::new().forget(true)), + deviceless.disconnect_with(DisconnectOptions::default().forget(true)), Err(Error::ForgetRequiresDrive) ); } diff --git a/tests/integration.rs b/tests/integration.rs index 0e5c728..b3a9eaa 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -31,7 +31,6 @@ fn drive_exists(letter: DriveLetter) -> bool { // Lovely Windows returns several statuses for a bad password. #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn wrong_password_fails_without_prompting() { let target = SmbTarget::new(required_env(SHARE)) .credentials(required_env(USERNAME), "definitely-the-wrong-password-1"); @@ -48,7 +47,6 @@ fn wrong_password_fails_without_prompting() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn nonexistent_share_fails() { let target = SmbTarget::new(r"\\thisisnotashare.local\Share-Name") .credentials(required_env(USERNAME), required_env(PASSWORD)); @@ -68,7 +66,6 @@ fn nonexistent_share_fails() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn deviceless_connect_and_reconnect_works() { let target = target(None); target.connect().unwrap(); @@ -79,7 +76,6 @@ fn deviceless_connect_and_reconnect_works() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn mounted_reconnect_fails_with_already_assigned() { let target = target(Some(DriveLetter::S)); target.connect().unwrap(); @@ -91,7 +87,6 @@ fn mounted_reconnect_fails_with_already_assigned() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn persistent_mapping_is_remembered_and_can_be_forgotten() { let target = target(Some(DriveLetter::R)); target @@ -99,7 +94,7 @@ fn persistent_mapping_is_remembered_and_can_be_forgotten() { .unwrap(); let remembered: Result, _> = enumerate::remembered().and_then(Iterator::collect); - let cleanup = target.disconnect_with(DisconnectOptions::new().force(true).forget(true)); + let cleanup = target.disconnect_with(DisconnectOptions::default().force(true).forget(true)); cleanup.unwrap(); assert!( @@ -114,7 +109,6 @@ fn persistent_mapping_is_remembered_and_can_be_forgotten() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn two_letters_to_the_same_share_work() { let one = target(Some(DriveLetter::S)); let two = target(Some(DriveLetter::T)); @@ -129,7 +123,6 @@ fn two_letters_to_the_same_share_work() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn force_disconnect_with_open_file_works() { let target = target(Some(DriveLetter::U)); target.connect().unwrap(); @@ -137,7 +130,7 @@ fn force_disconnect_with_open_file_works() { // Non-forced disconnect must refuse while a file is open. assert_eq!(target.disconnect(), Err(Error::Windows(ERROR_OPEN_FILES))); target - .disconnect_with(DisconnectOptions::new().force(true)) + .disconnect_with(DisconnectOptions::default().force(true)) .unwrap(); drop(file); assert!(!drive_exists(DriveLetter::U)); @@ -153,7 +146,6 @@ fn force_disconnect_with_open_file_works() { // device it owns, so dropping it must not tear down an independent deviceless // connection to the same resource. #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn guard_drop_leaves_other_connections_alone() { let deviceless = target(None); deviceless.connect().unwrap(); @@ -171,7 +163,6 @@ fn guard_drop_leaves_other_connections_alone() { // ── auto-assigned drive letter ────────────────────────────────────────────── #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn connect_auto_assigns_a_device() { let target = target(None); let access_name = target.connect_auto(ConnectOptions::new()).unwrap(); @@ -180,11 +171,10 @@ fn connect_auto_assigns_a_device() { "expected a device name, got {access_name:?}" ); assert!(std::path::Path::new(&format!(r"{access_name}\")).is_dir()); - cancel_connection(&access_name, DisconnectOptions::new()).unwrap(); + cancel_connection(&access_name, DisconnectOptions::default()).unwrap(); } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn connect_auto_guarded_owns_the_assigned_device() { let target = target(None); let device; @@ -203,7 +193,6 @@ fn connect_auto_guarded_owns_the_assigned_device() { // ── query ─────────────────────────────────────────────────────────────────── #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn get_connection_returns_the_remote_name() { let target = target(Some(DriveLetter::W)); target.connect().unwrap(); @@ -217,7 +206,6 @@ fn get_connection_returns_the_remote_name() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn get_user_returns_a_user() { let target = target(Some(DriveLetter::W)); target.connect().unwrap(); @@ -227,7 +215,6 @@ fn get_user_returns_a_user() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn get_universal_name_resolves_a_mounted_path() { let target = target(Some(DriveLetter::W)); target.connect().unwrap(); @@ -244,7 +231,6 @@ fn get_universal_name_resolves_a_mounted_path() { // ── enumerate ─────────────────────────────────────────────────────────────── #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn enumerate_connections_contains_the_share() { let target = target(Some(DriveLetter::X)); target.connect().unwrap(); @@ -260,7 +246,6 @@ fn enumerate_connections_contains_the_share() { } #[test] -#[ignore = "requires a live SMB share; set SAMBRS_TEST_* and run with --include-ignored"] fn enumerate_server_shares_contains_the_share() { // \\server\share -> \\server let full = required_env(SHARE);