Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 7 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
69 changes: 2 additions & 67 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 7 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,26 @@ name = "sambrs"
version = "0.2.0"
authors = ["Samuel Van der Stappen <shogun_einst.0i@icloud.com>"]
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"
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

println!("{}", std::fs::metadata(r"D:\")?.is_dir());

target.disconnect_with(DisconnectOptions::new().forget(true))?;
target.disconnect_with(DisconnectOptions::default().forget(true))?;
Ok(())
}
```
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
99 changes: 36 additions & 63 deletions src/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,9 +33,8 @@ pub struct NetResource {
#[derive(Debug)]
pub struct Resources {
handle: HANDLE,
batch: VecDeque<NetResource>,
/// 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<u64>,
finished: bool,
}
Expand All @@ -45,28 +43,15 @@ impl Iterator for Resources {
type Item = Result<NetResource>;

fn next(&mut self) -> Option<Self::Item> {
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::<u64>());
// SAFETY: `self.buf` outlives the call; `size` is its size in
// bytes.
Expand All @@ -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::<WNet::NETRESOURCEW>(),
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::<WNet::NETRESOURCEW>();
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::<u64>())];
}
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)))
}
}

Expand All @@ -136,14 +118,9 @@ impl Drop for Resources {
fn open(scope: u32, root_remote: Option<&str>) -> Result<Resources> {
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();
Expand All @@ -158,16 +135,12 @@ fn open(scope: u32, root_remote: Option<&str>) -> Result<Resources> {
)
};
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
Expand Down
Loading
Loading