From 29f48595b8cbc311c1f73aa4bc1ac667fc559bd4 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 14:21:48 +0000 Subject: [PATCH 01/17] build: upgrade windows-sys to 0.61 --- Cargo.lock | 69 ++---------------------------------------------------- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 68 deletions(-) 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..5dc8eb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ tracing = { version = "0.1", default-features = false } zeroize = "1" [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.60" +version = "0.61" features = [ "Win32_Foundation", "Win32_NetworkManagement_WNet", From 7a0593a4684231ea3fe9a92dea9816faec841b90 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 14:22:16 +0000 Subject: [PATCH 02/17] ci: use preinstalled Rust on Windows --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c74d416..2ec4516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,6 @@ jobs: 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 From bc5795dc2527c2a582d309b2e091fed8f4b672c7 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 14:22:29 +0000 Subject: [PATCH 03/17] ci: use preinstalled Rust for linting --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ec4516..917e2b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,10 +49,7 @@ jobs: 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 From 3c833bcf9d9e0c2b228a9f6e8c88a764b89ec04f Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 14:22:39 +0000 Subject: [PATCH 04/17] build: inline package description --- Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5dc8eb2..331ea9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,7 @@ 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"] From e40e4889281938c6d7ccd2fc511c458adde2dbee Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 15:53:16 +0000 Subject: [PATCH 05/17] refactor: trim unused public accessors --- src/options.rs | 10 +--------- src/target.rs | 6 ------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/options.rs b/src/options.rs index f4a893a..1aa9db3 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)) } } diff --git a/src/target.rs b/src/target.rs index 99d8e59..c3c267e 100644 --- a/src/target.rs +++ b/src/target.rs @@ -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 From 391c50541e6e2dc8acfcc0bb10cfd535ea056c75 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 15:54:06 +0000 Subject: [PATCH 06/17] refactor: simplify enumeration and MSRV setup --- .github/workflows/ci.yml | 9 +++------ src/enumerate.rs | 35 +++++++++++------------------------ 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 917e2b2..233e19e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,10 @@ jobs: 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) diff --git a/src/enumerate.rs b/src/enumerate.rs index 1e0b2bb..d74ccb3 100644 --- a/src/enumerate.rs +++ b/src/enumerate.rs @@ -10,7 +10,7 @@ //! # 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}; @@ -45,18 +45,13 @@ 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 self.batch.is_empty() && !self.finished { if let Err(e) = self.fill() { self.finished = true; return Some(Err(e)); } } + self.batch.pop_front().map(Ok) } } @@ -136,14 +131,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 +148,13 @@ 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, + batch: VecDeque::new(), + buf: vec![0u64; 2048], // 16 KiB to start; grows on demand + finished: false, + }) } /// Currently connected resources (`RESOURCE_CONNECTED`) — every active From 029369687113e24a48738e16b09bb1e21f1e184f Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 15:54:22 +0000 Subject: [PATCH 07/17] refactor: simplify guarded drive extraction --- src/target.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/target.rs b/src/target.rs index c3c267e..3450e57 100644 --- a/src/target.rs +++ b/src/target.rs @@ -251,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, From e37fabdf79d6a75829bad49e309194942368049e Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:10:28 +0000 Subject: [PATCH 08/17] refactor: trim remaining boilerplate --- Cargo.toml | 1 - README.md | 4 ++-- src/options.rs | 12 ++++-------- src/target.rs | 9 ++++----- tests/integration.rs | 6 +++--- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 331ea9f..5da09cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ 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" diff --git a/README.md b/README.md index c1e9e59..e4287f3 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(()) } ``` @@ -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/options.rs b/src/options.rs index 1aa9db3..36b52dc 100644 --- a/src/options.rs +++ b/src/options.rs @@ -114,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; @@ -188,9 +184,9 @@ mod tests { #[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); + assert!(!DisconnectOptions::default().forget); + assert!(!DisconnectOptions::default().force); + assert!(DisconnectOptions::default().forget(true).forget); + assert!(DisconnectOptions::default().force(true).force); } } diff --git a/src/target.rs b/src/target.rs index 3450e57..b7d3d42 100644 --- a/src/target.rs +++ b/src/target.rs @@ -289,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`]. @@ -337,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 @@ -383,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); } } @@ -451,7 +450,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..f2a6dd5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -99,7 +99,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!( @@ -137,7 +137,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)); @@ -180,7 +180,7 @@ 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] From f0a19b09256a7654c6c8fb9f348144b700d558f3 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:27:03 +0000 Subject: [PATCH 09/17] chore: remove redundant Windows feature --- Cargo.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5da09cc..4544cd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,7 @@ zeroize = "1" [target.'cfg(windows)'.dependencies.windows-sys] version = "0.61" -features = [ - "Win32_Foundation", - "Win32_NetworkManagement_WNet", -] +features = ["Win32_NetworkManagement_WNet"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" From 920e5cde878b9f10630ef415bef94e9f0962051e Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:38:25 +0000 Subject: [PATCH 10/17] Simplify CI configuration --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 233e19e..ffec3aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,6 @@ on: branches: [main] pull_request: -env: - CARGO_TERM_COLOR: always - jobs: test: name: Integration tests (Windows, real SMB share) @@ -16,7 +13,6 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - 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 From 10fe19c95f32f8140541e2cb37f357c8f9b1b62d Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:45:28 +0000 Subject: [PATCH 11/17] test: merge UTF-16 conversion cases --- src/strings.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/strings.rs b/src/strings.rs index f7fd66b..67d2b59 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -78,18 +78,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); } From b91dbdba3b8191499cd37199548ecd456b1d65bd Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:45:36 +0000 Subject: [PATCH 12/17] test: remove trivial disconnect option checks --- src/options.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/options.rs b/src/options.rs index 36b52dc..5dcf5a3 100644 --- a/src/options.rs +++ b/src/options.rs @@ -181,12 +181,4 @@ mod tests { | WNet::CONNECT_REQUIRE_PRIVACY ); } - - #[test] - fn disconnect_options_map() { - assert!(!DisconnectOptions::default().forget); - assert!(!DisconnectOptions::default().force); - assert!(DisconnectOptions::default().forget(true).forget); - assert!(DisconnectOptions::default().force(true).force); - } } From 2185f60499790969a8fe64573f5975b7ce117db7 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:45:51 +0000 Subject: [PATCH 13/17] test: focus target debug coverage --- src/target.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/target.rs b/src/target.rs index b7d3d42..3cd0a25 100644 --- a/src/target.rs +++ b/src/target.rs @@ -394,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")); From 25f6a9c4d0591d02c5deb3cc469cd20518190021 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:46:21 +0000 Subject: [PATCH 14/17] ci: merge static checks --- .github/workflows/ci.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffec3aa..3be38c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: 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 @@ -37,11 +37,6 @@ jobs: 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 - run: rustup target add x86_64-pc-windows-msvc - name: rustfmt From 205ecc40e93db084c579b56aa5ec1552da46c7f1 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:57:25 +0000 Subject: [PATCH 15/17] test: opt in to live SMB tests --- .github/workflows/ci.yml | 2 +- Cargo.toml | 4 ++++ README.md | 6 +++--- tests/integration.rs | 15 --------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3be38c3..ba2eb35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ 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! diff --git a/Cargo.toml b/Cargo.toml index 4544cd3..79c7162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ description = "Safe, opinionated Windows SMB client operations: connect, disconn # 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" diff --git a/README.md b/README.md index e4287f3..b210a2b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/tests/integration.rs b/tests/integration.rs index f2a6dd5..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 @@ -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(); @@ -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(); @@ -184,7 +175,6 @@ fn connect_auto_assigns_a_device() { } #[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); From a46e8a1bc75956be003dd2e988830159ba71115c Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:58:52 +0000 Subject: [PATCH 16/17] refactor: enumerate one resource at a time --- src/enumerate.rs | 70 +++++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/src/enumerate.rs b/src/enumerate.rs index d74ccb3..0ab5e9b 100644 --- a/src/enumerate.rs +++ b/src/enumerate.rs @@ -12,7 +12,6 @@ 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,23 +43,15 @@ impl Iterator for Resources { type Item = Result; fn next(&mut self) -> Option { - if self.batch.is_empty() && !self.finished { - if let Err(e) = self.fill() { - self.finished = true; - return Some(Err(e)); - } + if self.finished { + return None; } - self.batch.pop_front().map(Ok) - } -} -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. @@ -75,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))) } } @@ -151,7 +138,6 @@ fn open(scope: u32, root_remote: Option<&str>) -> Result { check_wnet(status)?; Ok(Resources { handle, - batch: VecDeque::new(), buf: vec![0u64; 2048], // 16 KiB to start; grows on demand finished: false, }) From c9e976c51e2b85fbfb2aea9720abdc3eb19a02d3 Mon Sep 17 00:00:00 2001 From: Samuel Van der Stappen Date: Tue, 14 Jul 2026 16:59:38 +0000 Subject: [PATCH 17/17] refactor: share optional wide pointer helper --- src/strings.rs | 12 ++---------- src/target.rs | 8 ++++---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/strings.rs b/src/strings.rs index 67d2b59..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) diff --git a/src/target.rs b/src/target.rs index 3cd0a25..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() } @@ -167,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, ) @@ -216,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(),