use crate::error::Result;
use crate::pack::{ReadCursor, WriteCursor};
use crate::Error;
pub const STYPE_DISKTREE: u32 = 0x0000_0000;
pub const STYPE_PRINTQ: u32 = 0x0000_0001;
pub const STYPE_DEVICE: u32 = 0x0000_0002;
pub const STYPE_IPC: u32 = 0x0000_0003;
pub const STYPE_SPECIAL: u32 = 0x8000_0000;
const STYPE_BASE_MASK: u32 = 0x0000_FFFF;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShareInfo {
pub name: String,
pub share_type: u32,
pub comment: String,
}
pub fn build_net_share_enum_all_stub(server_name: &str) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(128);
w.write_u32_le(0x0002_0000);
let name_utf16: Vec<u16> = server_name
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let char_count = name_utf16.len() as u32;
w.write_u32_le(char_count);
w.write_u32_le(0);
w.write_u32_le(char_count);
for &code_unit in &name_utf16 {
w.write_u16_le(code_unit);
}
w.align_to(4);
w.write_u32_le(1);
w.write_u32_le(1);
w.write_u32_le(0x0002_0004);
w.write_u32_le(0);
w.write_u32_le(0);
w.write_u32_le(0xFFFF_FFFF);
w.write_u32_le(0);
w.into_inner()
}
pub fn build_net_share_enum_all(call_id: u32, server_name: &str) -> Vec<u8> {
let stub = build_net_share_enum_all_stub(server_name);
super::build_request(call_id, 15, &stub)
}
pub fn parse_net_share_enum_all_response(data: &[u8]) -> Result<Vec<ShareInfo>> {
let stub = super::parse_response(data)?;
parse_net_share_enum_all_stub(stub)
}
fn parse_net_share_enum_all_stub(stub: &[u8]) -> Result<Vec<ShareInfo>> {
let mut r = ReadCursor::new(stub);
let level = r.read_u32_le()?;
if level != 1 {
return Err(Error::invalid_data(format!(
"expected share info level 1, got {level}"
)));
}
let discriminant = r.read_u32_le()?;
if discriminant != 1 {
return Err(Error::invalid_data(format!(
"expected union discriminant 1, got {discriminant}"
)));
}
let container_ptr = r.read_u32_le()?;
if container_ptr == 0 {
return Ok(Vec::new());
}
let count = r.read_u32_le()?;
let array_ptr = r.read_u32_le()?;
if array_ptr == 0 || count == 0 {
return Ok(Vec::new());
}
let max_count = r.read_u32_le()?;
if max_count < count {
return Err(Error::invalid_data(format!(
"array max_count ({max_count}) < entries ({count})"
)));
}
struct RawEntry {
name_ptr: u32,
share_type: u32,
comment_ptr: u32,
}
let mut entries = Vec::with_capacity(count as usize);
for _ in 0..count {
let name_ptr = r.read_u32_le()?;
let share_type = r.read_u32_le()?;
let comment_ptr = r.read_u32_le()?;
entries.push(RawEntry {
name_ptr,
share_type,
comment_ptr,
});
}
let mut shares = Vec::with_capacity(count as usize);
for entry in &entries {
let name = if entry.name_ptr != 0 {
read_ndr_string(&mut r)?
} else {
String::new()
};
let comment = if entry.comment_ptr != 0 {
read_ndr_string(&mut r)?
} else {
String::new()
};
shares.push(ShareInfo {
name,
share_type: entry.share_type,
comment,
});
}
Ok(shares)
}
fn read_ndr_string(r: &mut ReadCursor<'_>) -> Result<String> {
let _max_count = r.read_u32_le()?;
let _offset = r.read_u32_le()?;
let actual_count = r.read_u32_le()?;
if actual_count == 0 {
return Ok(String::new());
}
let byte_len = actual_count as usize * 2;
let s = r.read_utf16_le(byte_len)?;
let pos = r.position();
let padding = (4 - (pos % 4)) % 4;
if padding > 0 && r.remaining() >= padding {
r.skip(padding)?;
}
Ok(s.trim_end_matches('\0').to_string())
}
pub fn filter_disk_shares(shares: Vec<ShareInfo>) -> Vec<ShareInfo> {
shares
.into_iter()
.filter(|s| {
let base_type = s.share_type & STYPE_BASE_MASK;
let is_disk = base_type == STYPE_DISKTREE;
let is_special = (s.share_type & STYPE_SPECIAL) != 0;
let ends_with_dollar = s.name.ends_with('$');
is_disk && !is_special && !ends_with_dollar
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_request_has_opnum_15() {
let pdu = build_net_share_enum_all(1, r"\\server");
let opnum = u16::from_le_bytes([pdu[22], pdu[23]]);
assert_eq!(opnum, 15);
}
#[test]
fn build_request_stub_contains_server_name() {
let stub = build_net_share_enum_all_stub(r"\\server");
let expected_utf16: Vec<u8> = r"\\server"
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let found = stub
.windows(expected_utf16.len())
.any(|window| window == expected_utf16.as_slice());
assert!(found, "stub should contain the server name in UTF-16LE");
}
#[test]
fn parse_response_with_three_shares() {
let response_pdu = build_test_enum_response(&[
("Documents", STYPE_DISKTREE, "Shared docs"),
("IPC$", STYPE_IPC | STYPE_SPECIAL, "Remote IPC"),
("C$", STYPE_DISKTREE | STYPE_SPECIAL, "Default share"),
]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 3);
assert_eq!(shares[0].name, "Documents");
assert_eq!(shares[0].share_type, STYPE_DISKTREE);
assert_eq!(shares[0].comment, "Shared docs");
assert_eq!(shares[1].name, "IPC$");
assert_eq!(shares[2].name, "C$");
}
#[test]
fn filter_keeps_disk_shares() {
let shares = vec![
ShareInfo {
name: "Documents".to_string(),
share_type: STYPE_DISKTREE,
comment: "Shared docs".to_string(),
},
ShareInfo {
name: "Photos".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
},
];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 2);
}
#[test]
fn filter_removes_ipc() {
let shares = vec![ShareInfo {
name: "IPC$".to_string(),
share_type: STYPE_IPC | STYPE_SPECIAL,
comment: "Remote IPC".to_string(),
}];
let filtered = filter_disk_shares(shares);
assert!(filtered.is_empty());
}
#[test]
fn filter_removes_admin_shares() {
let shares = vec![
ShareInfo {
name: "C$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Default share".to_string(),
},
ShareInfo {
name: "ADMIN$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Remote Admin".to_string(),
},
];
let filtered = filter_disk_shares(shares);
assert!(filtered.is_empty());
}
#[test]
fn filter_mixed_shares() {
let shares = vec![
ShareInfo {
name: "Documents".to_string(),
share_type: STYPE_DISKTREE,
comment: "Shared docs".to_string(),
},
ShareInfo {
name: "IPC$".to_string(),
share_type: STYPE_IPC | STYPE_SPECIAL,
comment: "Remote IPC".to_string(),
},
ShareInfo {
name: "C$".to_string(),
share_type: STYPE_DISKTREE | STYPE_SPECIAL,
comment: "Default share".to_string(),
},
ShareInfo {
name: "Photos".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
},
ShareInfo {
name: "Printer".to_string(),
share_type: STYPE_PRINTQ,
comment: "Office printer".to_string(),
},
];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "Documents");
assert_eq!(filtered[1].name, "Photos");
}
#[test]
fn parse_empty_share_list() {
let response_pdu = build_test_enum_response(&[]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert!(shares.is_empty());
}
#[test]
fn parse_share_with_unicode_name() {
let response_pdu = build_test_enum_response(&[(
"\u{00C4}rchive",
STYPE_DISKTREE,
"Archiv f\u{00FC}r Dateien",
)]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 1);
assert_eq!(shares[0].name, "\u{00C4}rchive");
assert_eq!(shares[0].comment, "Archiv f\u{00FC}r Dateien");
}
#[test]
fn parse_share_with_cjk_characters() {
let response_pdu = build_test_enum_response(&[(
"\u{5171}\u{6709}",
STYPE_DISKTREE,
"\u{5171}\u{6709}\u{30D5}\u{30A9}\u{30EB}\u{30C0}",
)]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 1);
assert_eq!(shares[0].name, "\u{5171}\u{6709}");
assert_eq!(
shares[0].comment,
"\u{5171}\u{6709}\u{30D5}\u{30A9}\u{30EB}\u{30C0}"
);
}
#[test]
fn roundtrip_build_and_parse() {
let _request = build_net_share_enum_all(1, r"\\testserver");
let response_pdu = build_test_enum_response(&[
("Share1", STYPE_DISKTREE, "First share"),
("Share2", STYPE_DISKTREE, "Second share"),
]);
let shares = parse_net_share_enum_all_response(&response_pdu).unwrap();
assert_eq!(shares.len(), 2);
assert_eq!(shares[0].name, "Share1");
assert_eq!(shares[0].comment, "First share");
assert_eq!(shares[1].name, "Share2");
assert_eq!(shares[1].comment, "Second share");
}
#[test]
fn filter_preserves_non_dollar_disk_shares_only() {
let shares = vec![ShareInfo {
name: "My$hare".to_string(),
share_type: STYPE_DISKTREE,
comment: String::new(),
}];
let filtered = filter_disk_shares(shares);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "My$hare");
}
fn write_ndr_string(w: &mut WriteCursor, s: &str) {
let utf16: Vec<u16> = s.encode_utf16().chain(std::iter::once(0)).collect();
let char_count = utf16.len() as u32;
w.write_u32_le(char_count); w.write_u32_le(0); w.write_u32_le(char_count); for &code_unit in &utf16 {
w.write_u16_le(code_unit);
}
w.align_to(4);
}
fn build_test_enum_response(shares: &[(&str, u32, &str)]) -> Vec<u8> {
let stub = build_test_enum_stub(shares);
build_test_response_pdu(1, &stub)
}
fn build_test_enum_stub(shares: &[(&str, u32, &str)]) -> Vec<u8> {
let mut w = WriteCursor::with_capacity(512);
let count = shares.len() as u32;
w.write_u32_le(1);
w.write_u32_le(1);
if count == 0 {
w.write_u32_le(0);
w.write_u32_le(0);
w.write_u32_le(0);
w.write_u32_le(0);
return w.into_inner();
}
w.write_u32_le(0x0002_0000);
w.write_u32_le(count); w.write_u32_le(0x0002_0004);
w.write_u32_le(count);
for (i, &(_, share_type, _)) in shares.iter().enumerate() {
w.write_u32_le(0x0002_0008 + (i as u32) * 2); w.write_u32_le(share_type);
w.write_u32_le(0x0002_0108 + (i as u32) * 2); }
for &(name, _, comment) in shares {
write_ndr_string(&mut w, name);
write_ndr_string(&mut w, comment);
}
w.write_u32_le(count);
w.write_u32_le(0);
w.write_u32_le(0);
w.into_inner()
}
fn build_test_response_pdu(call_id: u32, stub: &[u8]) -> Vec<u8> {
use crate::pack::WriteCursor;
let mut w = WriteCursor::with_capacity(24 + stub.len());
w.write_u8(5); w.write_u8(0); w.write_u8(2); w.write_u8(0x03); w.write_bytes(&[0x10, 0x00, 0x00, 0x00]); let frag_len_pos = w.position();
w.write_u16_le(0); w.write_u16_le(0); w.write_u32_le(call_id);
w.write_u32_le(stub.len() as u32); w.write_u16_le(0); w.write_u8(0); w.write_u8(0);
w.write_bytes(stub);
let total_len = w.position();
w.set_u16_le_at(frag_len_pos, total_len as u16);
w.into_inner()
}
}