Skip to main content

smolder_core/
facade.rs

1//! High-level embedded client facade built on top of the typestate SMB client.
2//!
3//! This module is the intended additive entry point for users who want a
4//! friendlier `connect -> authenticate -> tree connect -> file workflow` path
5//! without dropping directly into raw typestate orchestration.
6
7use rand::random;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use smolder_proto::rpc::SyntaxId;
11use smolder_proto::smb::compression::{CompressionAlgorithm, CompressionCapabilityFlags};
12use smolder_proto::smb::smb2::{
13    CloseRequest, CompressionCapabilities, CreateDisposition, CreateOptions, CreateRequest,
14    Dialect, DirectoryInformationEntry, DispositionInformation, EchoResponse, FileAttributes,
15    FileBasicInformation, FileId, FileInfoClass, FileStandardInformation, FlushRequest,
16    GlobalCapabilities, QueryDirectoryFlags, QueryDirectoryRequest, QueryInfoRequest, ReadRequest,
17    RenameInformation, SessionId, SetInfoRequest, ShareAccess, SigningMode, TreeConnectRequest,
18    TreeId, WriteRequest,
19};
20
21use crate::auth::NtlmCredentials;
22#[cfg(feature = "kerberos-api")]
23use crate::auth::{KerberosCredentials, KerberosTarget};
24use crate::client::{
25    Authenticated, Connection, DurableHandle, DurableOpenOptions, ResilientHandle, TreeConnected,
26};
27use crate::error::CoreError;
28use crate::lsarpc::LsarpcClient;
29use crate::pipe::{connect_session, NamedPipe, PipeAccess, SmbSessionConfig};
30#[cfg(feature = "quic")]
31use crate::pipe::{connect_session_quic, connect_tree_quic};
32use crate::rpc::PipeRpcClient;
33use crate::samr::SamrClient;
34use crate::srvsvc::SrvsvcClient;
35#[cfg(feature = "quic")]
36use crate::transport::QuicTransport;
37use crate::transport::{SmbTransport, TokioTcpTransport, TransportProtocol, TransportTarget};
38const MAX_IO_CHUNK_SIZE: usize = u16::MAX as usize;
39const FILE_READ_DATA: u32 = 0x0000_0001;
40const FILE_WRITE_DATA: u32 = 0x0000_0002;
41const FILE_APPEND_DATA: u32 = 0x0000_0004;
42const FILE_READ_EA: u32 = 0x0000_0008;
43const FILE_WRITE_EA: u32 = 0x0000_0010;
44const FILE_READ_ATTRIBUTES: u32 = 0x0000_0080;
45const FILE_WRITE_ATTRIBUTES: u32 = 0x0000_0100;
46const FILE_LIST_DIRECTORY: u32 = 0x0000_0001;
47const DELETE: u32 = 0x0001_0000;
48const READ_CONTROL: u32 = 0x0002_0000;
49const SYNCHRONIZE: u32 = 0x0010_0000;
50const WINDOWS_TICK: u64 = 10_000_000;
51const SEC_TO_UNIX_EPOCH: u64 = 11_644_473_600;
52const DIRECTORY_QUERY_BUFFER_SIZE: u32 = 64 * 1024;
53
54#[derive(Debug, Clone)]
55enum BuilderAuth {
56    Ntlm(NtlmCredentials),
57    #[cfg(feature = "kerberos-api")]
58    Kerberos {
59        credentials: KerberosCredentials,
60        target: KerberosTarget,
61    },
62}
63
64/// Builder for the high-level SMB client facade.
65#[derive(Debug, Clone)]
66pub struct ClientBuilder {
67    target: TransportTarget,
68    auth: Option<BuilderAuth>,
69    signing_mode: SigningMode,
70    capabilities: GlobalCapabilities,
71    dialects: Vec<Dialect>,
72    client_guid: [u8; 16],
73    compression: Option<CompressionCapabilities>,
74}
75
76impl ClientBuilder {
77    /// Creates a new client builder for the target server.
78    #[must_use]
79    pub fn new(server: impl Into<String>) -> Self {
80        Self {
81            target: TransportTarget::tcp(server),
82            auth: None,
83            signing_mode: SigningMode::ENABLED,
84            capabilities: GlobalCapabilities::LARGE_MTU
85                | GlobalCapabilities::LEASING
86                | GlobalCapabilities::ENCRYPTION,
87            dialects: vec![Dialect::Smb210, Dialect::Smb302, Dialect::Smb311],
88            client_guid: random(),
89            compression: None,
90        }
91    }
92
93    /// Overrides the target SMB TCP port.
94    #[must_use]
95    pub fn with_port(mut self, port: u16) -> Self {
96        self.target = self.target.with_port(port);
97        self
98    }
99
100    /// Overrides the full transport target.
101    #[must_use]
102    pub fn with_transport_target(mut self, target: TransportTarget) -> Self {
103        self.target = target;
104        self
105    }
106
107    /// Overrides the transport dial host or IP address while preserving the logical SMB server name.
108    #[must_use]
109    pub fn with_connect_host(mut self, connect_host: impl Into<String>) -> Self {
110        self.target = self.target.with_connect_host(connect_host);
111        self
112    }
113
114    /// Overrides the TLS server name used by SMB over QUIC.
115    #[must_use]
116    pub fn with_tls_server_name(mut self, tls_server_name: impl Into<String>) -> Self {
117        self.target = self.target.with_tls_server_name(tls_server_name);
118        self
119    }
120
121    /// Overrides the SMB signing mode sent during negotiate.
122    #[must_use]
123    pub fn with_signing_mode(mut self, signing_mode: SigningMode) -> Self {
124        self.signing_mode = signing_mode;
125        self
126    }
127
128    /// Overrides the advertised SMB capabilities.
129    #[must_use]
130    pub fn with_capabilities(mut self, capabilities: GlobalCapabilities) -> Self {
131        self.capabilities = capabilities;
132        self
133    }
134
135    /// Overrides the negotiate dialect list.
136    #[must_use]
137    pub fn with_dialects(mut self, dialects: Vec<Dialect>) -> Self {
138        self.dialects = dialects;
139        self
140    }
141
142    /// Overrides the client GUID sent during negotiate.
143    #[must_use]
144    pub fn with_client_guid(mut self, client_guid: [u8; 16]) -> Self {
145        self.client_guid = client_guid;
146        self
147    }
148
149    /// Overrides the advertised SMB compression capabilities.
150    #[must_use]
151    pub fn with_compression_capabilities(mut self, compression: CompressionCapabilities) -> Self {
152        self.compression = Some(compression);
153        self
154    }
155
156    /// Advertises unchained SMB compression with the provided algorithms.
157    #[must_use]
158    pub fn with_compression_algorithms(
159        mut self,
160        compression_algorithms: Vec<CompressionAlgorithm>,
161    ) -> Self {
162        self.compression = Some(CompressionCapabilities {
163            compression_algorithms,
164            flags: CompressionCapabilityFlags::empty(),
165        });
166        self
167    }
168
169    /// Configures NTLM credentials for the client.
170    #[must_use]
171    pub fn with_ntlm_credentials(mut self, credentials: NtlmCredentials) -> Self {
172        self.auth = Some(BuilderAuth::Ntlm(credentials));
173        self
174    }
175
176    /// Configures Kerberos credentials for the client.
177    #[cfg(feature = "kerberos-api")]
178    #[cfg_attr(
179        docsrs,
180        doc(cfg(any(feature = "kerberos", feature = "kerberos-gssapi")))
181    )]
182    #[must_use]
183    pub fn with_kerberos_credentials(
184        mut self,
185        credentials: KerberosCredentials,
186        target: KerberosTarget,
187    ) -> Self {
188        self.auth = Some(BuilderAuth::Kerberos {
189            credentials,
190            target,
191        });
192        self
193    }
194
195    /// Builds a reusable high-level client.
196    pub fn build(self) -> Result<Client, CoreError> {
197        let auth = self.auth.ok_or(CoreError::InvalidInput(
198            "client builder requires NTLM or Kerberos credentials",
199        ))?;
200
201        let config = match auth {
202            BuilderAuth::Ntlm(credentials) => {
203                SmbSessionConfig::new(self.target.server().to_owned(), credentials)
204            }
205            #[cfg(feature = "kerberos-api")]
206            BuilderAuth::Kerberos {
207                credentials,
208                target,
209            } => SmbSessionConfig::kerberos(self.target.server().to_owned(), credentials, target),
210        }
211        .with_transport_target(self.target)
212        .with_signing_mode(self.signing_mode)
213        .with_capabilities(self.capabilities)
214        .with_dialects(self.dialects)
215        .with_client_guid(self.client_guid);
216        let config = if let Some(compression) = self.compression {
217            config.with_compression_capabilities(compression)
218        } else {
219            config
220        };
221
222        Ok(Client::from_session_config(config))
223    }
224}
225
226/// High-level embedded SMB client facade.
227#[derive(Debug, Clone)]
228pub struct Client {
229    config: SmbSessionConfig,
230}
231
232impl Client {
233    /// Starts a new builder for the target server.
234    #[must_use]
235    pub fn builder(server: impl Into<String>) -> ClientBuilder {
236        ClientBuilder::new(server)
237    }
238
239    /// Wraps an existing session configuration as a high-level client.
240    #[must_use]
241    pub fn from_session_config(config: SmbSessionConfig) -> Self {
242        Self { config }
243    }
244
245    /// Returns the underlying session configuration.
246    #[must_use]
247    pub fn session_config(&self) -> &SmbSessionConfig {
248        &self.config
249    }
250
251    /// Consumes the client and returns the underlying session configuration.
252    #[must_use]
253    pub fn into_session_config(self) -> SmbSessionConfig {
254        self.config
255    }
256
257    /// Returns the configured SMB server host name or IP address.
258    #[must_use]
259    pub fn server(&self) -> &str {
260        self.config.server()
261    }
262
263    /// Returns the configured SMB TCP port.
264    #[must_use]
265    pub fn port(&self) -> u16 {
266        self.config.port()
267    }
268
269    /// Returns the configured dial host or IP address.
270    #[must_use]
271    pub fn connect_host(&self) -> &str {
272        self.config.connect_host()
273    }
274
275    /// Returns the configured TLS server name used by SMB over QUIC.
276    #[must_use]
277    pub fn tls_server_name(&self) -> &str {
278        self.config.tls_server_name()
279    }
280
281    /// Returns the configured transport target.
282    #[must_use]
283    pub fn transport_target(&self) -> &TransportTarget {
284        self.config.transport_target()
285    }
286
287    /// Returns the configured transport protocol.
288    #[must_use]
289    pub fn transport_protocol(&self) -> TransportProtocol {
290        self.config.transport_protocol()
291    }
292
293    /// Connects and authenticates an SMB session.
294    pub async fn connect(&self) -> Result<Session, CoreError> {
295        let connection = connect_session(&self.config).await?;
296        Ok(Session {
297            server: self.config.server().to_owned(),
298            connection,
299        })
300    }
301
302    /// Connects and authenticates an SMB session over QUIC.
303    #[cfg(feature = "quic")]
304    #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
305    pub async fn connect_quic(&self) -> Result<Session<QuicTransport>, CoreError> {
306        if self.config.transport_protocol() != TransportProtocol::Quic {
307            return Err(CoreError::InvalidInput(
308                "client is not configured for an SMB over QUIC transport target",
309            ));
310        }
311        let connection = connect_session_quic(&self.config).await?;
312        Ok(Session {
313            server: self.config.server().to_owned(),
314            connection,
315        })
316    }
317
318    /// Connects, authenticates, and tree-connects to the requested share.
319    pub async fn connect_share(&self, share: &str) -> Result<Share, CoreError> {
320        self.connect().await?.connect_share(share).await
321    }
322
323    /// Connects, authenticates, and tree-connects to the requested share over QUIC.
324    #[cfg(feature = "quic")]
325    #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
326    pub async fn connect_share_quic(&self, share: &str) -> Result<Share<QuicTransport>, CoreError> {
327        if self.config.transport_protocol() != TransportProtocol::Quic {
328            return Err(CoreError::InvalidInput(
329                "client is not configured for an SMB over QUIC transport target",
330            ));
331        }
332        let connection = connect_tree_quic(&self.config, share).await?;
333        Ok(Share {
334            server: self.config.server().to_owned(),
335            name: normalize_share_name(share)?,
336            connection,
337        })
338    }
339
340    /// Connects directly to `IPC$`.
341    pub async fn connect_ipc(&self) -> Result<Share, CoreError> {
342        self.connect_share("IPC$").await
343    }
344
345    /// Connects directly to `IPC$` over QUIC.
346    #[cfg(feature = "quic")]
347    #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
348    pub async fn connect_ipc_quic(&self) -> Result<Share<QuicTransport>, CoreError> {
349        self.connect_share_quic("IPC$").await
350    }
351
352    /// Connects, authenticates, opens `IPC$`, and binds a typed `lsarpc` client.
353    pub async fn connect_lsarpc(&self) -> Result<LsarpcClient, CoreError> {
354        self.connect().await?.connect_lsarpc().await
355    }
356
357    /// Connects, authenticates, opens `IPC$`, and binds a typed `lsarpc` client over QUIC.
358    #[cfg(feature = "quic")]
359    #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
360    pub async fn connect_lsarpc_quic(&self) -> Result<LsarpcClient<QuicTransport>, CoreError> {
361        self.connect_quic().await?.connect_lsarpc().await
362    }
363
364    /// Connects, authenticates, opens `IPC$`, and binds a typed `srvsvc` client.
365    pub async fn connect_srvsvc(&self) -> Result<SrvsvcClient, CoreError> {
366        self.connect().await?.connect_srvsvc().await
367    }
368
369    /// Connects, authenticates, opens `IPC$`, and binds a typed `srvsvc` client over QUIC.
370    #[cfg(feature = "quic")]
371    #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
372    pub async fn connect_srvsvc_quic(&self) -> Result<SrvsvcClient<QuicTransport>, CoreError> {
373        self.connect_quic().await?.connect_srvsvc().await
374    }
375}
376
377/// Authenticated SMB session returned by the high-level client.
378#[derive(Debug)]
379pub struct Session<T = TokioTcpTransport> {
380    server: String,
381    connection: Connection<T, Authenticated>,
382}
383
384impl<T> Session<T>
385where
386    T: SmbTransport + Send,
387{
388    /// Returns the target SMB server for this session.
389    #[must_use]
390    pub fn server(&self) -> &str {
391        &self.server
392    }
393
394    /// Returns the active SMB session identifier.
395    #[must_use]
396    pub fn session_id(&self) -> SessionId {
397        self.connection.session_id()
398    }
399
400    /// Returns the exported SMB session key, if the auth mechanism established one.
401    #[must_use]
402    pub fn session_key(&self) -> Option<&[u8]> {
403        self.connection.session_key()
404    }
405
406    /// Returns the wrapped authenticated connection.
407    #[must_use]
408    pub fn connection(&self) -> &Connection<T, Authenticated> {
409        &self.connection
410    }
411
412    /// Returns a mutable reference to the wrapped authenticated connection.
413    #[must_use]
414    pub fn connection_mut(&mut self) -> &mut Connection<T, Authenticated> {
415        &mut self.connection
416    }
417
418    /// Consumes the session wrapper and returns the underlying authenticated connection.
419    #[must_use]
420    pub fn into_connection(self) -> Connection<T, Authenticated> {
421        self.connection
422    }
423
424    /// Performs an `ECHO` request against the active SMB session.
425    pub async fn echo(&mut self) -> Result<EchoResponse, CoreError> {
426        self.connection.echo().await
427    }
428
429    /// Tree-connects to the requested share.
430    pub async fn connect_share(self, share: &str) -> Result<Share<T>, CoreError> {
431        let normalized_share = normalize_share_name(share)?;
432        let unc = format!(r"\\{}\{}", self.server, normalized_share);
433        let connection = self
434            .connection
435            .tree_connect(&TreeConnectRequest::from_unc(&unc))
436            .await?;
437        Ok(Share {
438            server: self.server,
439            name: normalized_share,
440            connection,
441        })
442    }
443
444    /// Tree-connects directly to `IPC$`.
445    pub async fn connect_ipc(self) -> Result<Share<T>, CoreError> {
446        self.connect_share("IPC$").await
447    }
448
449    /// Opens a named pipe on `IPC$`.
450    pub async fn connect_pipe(
451        self,
452        pipe_name: &str,
453        access: PipeAccess,
454    ) -> Result<NamedPipe<T>, CoreError> {
455        self.connect_ipc().await?.open_pipe(pipe_name, access).await
456    }
457
458    /// Opens a named pipe on `IPC$` and wraps it as an RPC transport.
459    pub async fn connect_rpc_pipe(
460        self,
461        pipe_name: &str,
462        access: PipeAccess,
463    ) -> Result<PipeRpcClient<T>, CoreError> {
464        let pipe = self.connect_pipe(pipe_name, access).await?;
465        Ok(PipeRpcClient::new(pipe))
466    }
467
468    /// Opens a named pipe on `IPC$`, performs an RPC bind, and returns the bound RPC client.
469    pub async fn bind_rpc(
470        self,
471        pipe_name: &str,
472        context_id: u16,
473        abstract_syntax: SyntaxId,
474    ) -> Result<PipeRpcClient<T>, CoreError> {
475        let mut rpc = self
476            .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
477            .await?;
478        rpc.bind_context(context_id, abstract_syntax).await?;
479        Ok(rpc)
480    }
481
482    /// Opens `\\PIPE\\srvsvc` on `IPC$`, performs the bind, and returns a typed client.
483    pub async fn connect_srvsvc(self) -> Result<SrvsvcClient<T>, CoreError> {
484        let rpc = self
485            .connect_rpc_pipe("srvsvc", PipeAccess::ReadWrite)
486            .await?;
487        SrvsvcClient::bind(rpc).await
488    }
489
490    /// Opens `\\PIPE\\lsarpc` on `IPC$`, performs the bind/open, and returns a typed client.
491    pub async fn connect_lsarpc(self) -> Result<LsarpcClient<T>, CoreError> {
492        let rpc = self
493            .connect_rpc_pipe("lsarpc", PipeAccess::ReadWrite)
494            .await?;
495        LsarpcClient::bind(rpc).await
496    }
497
498    /// Opens a caller-selected SAMR-capable pipe on `IPC$`, performs the bind/connect, and returns a typed client.
499    pub async fn connect_samr_pipe(self, pipe_name: &str) -> Result<SamrClient<T>, CoreError> {
500        let rpc = self
501            .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
502            .await?;
503        SamrClient::bind(rpc).await
504    }
505
506    /// Opens the default SAMR endpoint on `IPC$`, performs the bind/connect, and returns a typed client.
507    pub async fn connect_samr(self) -> Result<SamrClient<T>, CoreError> {
508        self.connect_samr_pipe("lsarpc").await
509    }
510
511    /// Logs off the authenticated SMB session.
512    pub async fn logoff(self) -> Result<(), CoreError> {
513        let _ = self.connection.logoff().await?;
514        Ok(())
515    }
516}
517
518/// Tree-connected SMB share returned by the high-level client/session facade.
519#[derive(Debug)]
520pub struct Share<T = TokioTcpTransport> {
521    server: String,
522    name: String,
523    connection: Connection<T, TreeConnected>,
524}
525
526impl<T> Share<T>
527where
528    T: SmbTransport + Send,
529{
530    /// Returns the target SMB server for this tree connection.
531    #[must_use]
532    pub fn server(&self) -> &str {
533        &self.server
534    }
535
536    /// Returns the connected SMB share name.
537    #[must_use]
538    pub fn name(&self) -> &str {
539        &self.name
540    }
541
542    /// Returns the active SMB session identifier.
543    #[must_use]
544    pub fn session_id(&self) -> SessionId {
545        self.connection.session_id()
546    }
547
548    /// Returns the active SMB tree identifier.
549    #[must_use]
550    pub fn tree_id(&self) -> TreeId {
551        self.connection.tree_id()
552    }
553
554    /// Returns the exported SMB session key, if the auth mechanism established one.
555    #[must_use]
556    pub fn session_key(&self) -> Option<&[u8]> {
557        self.connection.session_key()
558    }
559
560    /// Returns the wrapped tree-connected connection.
561    #[must_use]
562    pub fn connection(&self) -> &Connection<T, TreeConnected> {
563        &self.connection
564    }
565
566    /// Returns a mutable reference to the wrapped tree-connected connection.
567    #[must_use]
568    pub fn connection_mut(&mut self) -> &mut Connection<T, TreeConnected> {
569        &mut self.connection
570    }
571
572    /// Consumes the share wrapper and returns the underlying tree-connected connection.
573    #[must_use]
574    pub fn into_connection(self) -> Connection<T, TreeConnected> {
575        self.connection
576    }
577
578    /// Opens a file on the current tree and returns a high-level file wrapper.
579    pub async fn open(mut self, path: &str, options: OpenOptions) -> Result<File<T>, CoreError> {
580        let normalized_path = normalize_share_path(path)?;
581        let dialect = self.connection.state().negotiated.dialect_revision;
582        let create_request = options.to_create_request(&normalized_path)?;
583        let (durable_handle, file_id) = if let Some(durable) = options.durable_options(dialect) {
584            let durable_handle = self
585                .connection
586                .create_durable(&create_request, durable)
587                .await?;
588            (Some(durable_handle.clone()), durable_handle.file_id())
589        } else {
590            let response = self.connection.create(&create_request).await?;
591            (None, response.file_id)
592        };
593
594        let resilient_handle = if let Some(timeout) = options.resilient_timeout {
595            Some(self.connection.request_resiliency(file_id, timeout).await?)
596        } else {
597            None
598        };
599
600        Ok(File {
601            share: self,
602            path: normalized_path,
603            file_id,
604            durable_handle,
605            resilient_handle,
606        })
607    }
608
609    /// Opens a named pipe on the current tree, which is usually `IPC$`.
610    pub async fn open_pipe(
611        self,
612        pipe_name: &str,
613        access: PipeAccess,
614    ) -> Result<NamedPipe<T>, CoreError> {
615        let normalized_pipe = normalize_pipe_name(pipe_name)?;
616        NamedPipe::open(self.connection, &normalized_pipe, access).await
617    }
618
619    /// Opens a named pipe on the current tree and wraps it as an RPC transport.
620    pub async fn connect_rpc_pipe(
621        self,
622        pipe_name: &str,
623        access: PipeAccess,
624    ) -> Result<PipeRpcClient<T>, CoreError> {
625        let pipe = self.open_pipe(pipe_name, access).await?;
626        Ok(PipeRpcClient::new(pipe))
627    }
628
629    /// Opens a named pipe on the current tree, performs an RPC bind, and returns the bound client.
630    pub async fn bind_rpc(
631        self,
632        pipe_name: &str,
633        context_id: u16,
634        abstract_syntax: SyntaxId,
635    ) -> Result<PipeRpcClient<T>, CoreError> {
636        let mut rpc = self
637            .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
638            .await?;
639        rpc.bind_context(context_id, abstract_syntax).await?;
640        Ok(rpc)
641    }
642
643    /// Opens `\\PIPE\\srvsvc` on the current tree, performs the bind, and returns a typed client.
644    pub async fn connect_srvsvc(self) -> Result<SrvsvcClient<T>, CoreError> {
645        let rpc = self
646            .connect_rpc_pipe("srvsvc", PipeAccess::ReadWrite)
647            .await?;
648        SrvsvcClient::bind(rpc).await
649    }
650
651    /// Opens `\\PIPE\\lsarpc` on the current tree, performs the bind/open, and returns a typed client.
652    pub async fn connect_lsarpc(self) -> Result<LsarpcClient<T>, CoreError> {
653        let rpc = self
654            .connect_rpc_pipe("lsarpc", PipeAccess::ReadWrite)
655            .await?;
656        LsarpcClient::bind(rpc).await
657    }
658
659    /// Opens a caller-selected SAMR-capable pipe on the current tree, performs the bind/connect, and returns a typed client.
660    pub async fn connect_samr_pipe(self, pipe_name: &str) -> Result<SamrClient<T>, CoreError> {
661        let rpc = self
662            .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
663            .await?;
664        SamrClient::bind(rpc).await
665    }
666
667    /// Opens the default SAMR endpoint on the current tree, performs the bind/connect, and returns a typed client.
668    pub async fn connect_samr(self) -> Result<SamrClient<T>, CoreError> {
669        self.connect_samr_pipe("lsarpc").await
670    }
671
672    /// Reads the full contents of a file on the current tree.
673    pub async fn read(&mut self, path: &str) -> Result<Vec<u8>, CoreError> {
674        let normalized_path = normalize_share_path(path)?;
675        let create_request = OpenOptions::new()
676            .read(true)
677            .to_create_request(&normalized_path)?;
678        let response = self.connection.create(&create_request).await?;
679        let file_id = response.file_id;
680        let size = self.stat_by_id(file_id).await?.size;
681        let mut output = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
682        let mut offset = 0u64;
683
684        while offset < size {
685            let remaining = size - offset;
686            let chunk_len = remaining.min(MAX_IO_CHUNK_SIZE as u64) as u32;
687            let response = self
688                .connection
689                .read(&ReadRequest::for_file(file_id, offset, chunk_len))
690                .await?;
691            if response.data.is_empty() {
692                break;
693            }
694            offset = offset.saturating_add(response.data.len() as u64);
695            output.extend_from_slice(&response.data);
696        }
697
698        self.connection
699            .close(&CloseRequest { flags: 0, file_id })
700            .await?;
701        Ok(output)
702    }
703
704    /// Reads the full contents of a file on the current tree.
705    ///
706    /// This is an ergonomic alias for [`Share::read`] that matches the
707    /// higher-level "get/put" workflow commonly used by embedded clients.
708    pub async fn get(&mut self, path: &str) -> Result<Vec<u8>, CoreError> {
709        self.read(path).await
710    }
711
712    /// Writes the full contents of a file on the current tree, creating it when absent.
713    pub async fn write(&mut self, path: &str, data: &[u8]) -> Result<(), CoreError> {
714        let normalized_path = normalize_share_path(path)?;
715        let create_request = OpenOptions::new()
716            .write(true)
717            .create(true)
718            .truncate(true)
719            .to_create_request(&normalized_path)?;
720        let response = self.connection.create(&create_request).await?;
721        let file_id = response.file_id;
722        let mut offset = 0u64;
723
724        while (offset as usize) < data.len() {
725            let chunk_end = (offset as usize + MAX_IO_CHUNK_SIZE).min(data.len());
726            self.connection
727                .write(&WriteRequest::for_file(
728                    file_id,
729                    offset,
730                    data[offset as usize..chunk_end].to_vec(),
731                ))
732                .await?;
733            offset = chunk_end as u64;
734        }
735
736        self.connection
737            .flush(&FlushRequest::for_file(file_id))
738            .await?;
739        self.connection
740            .close(&CloseRequest { flags: 0, file_id })
741            .await?;
742        Ok(())
743    }
744
745    /// Writes the full contents of a file on the current tree, creating it when absent.
746    ///
747    /// This is an ergonomic alias for [`Share::write`] that matches the
748    /// higher-level "get/put" workflow commonly used by embedded clients.
749    pub async fn put(&mut self, path: &str, data: &[u8]) -> Result<(), CoreError> {
750        self.write(path, data).await
751    }
752
753    /// Queries file metadata on the current tree.
754    pub async fn stat(&mut self, path: &str) -> Result<FileMetadata, CoreError> {
755        let normalized_path = normalize_share_path(path)?;
756        let mut create_request = CreateRequest::from_path(&normalized_path);
757        create_request.desired_access = FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE;
758        create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
759        create_request.create_disposition = CreateDisposition::Open;
760        create_request.create_options = CreateOptions::NON_DIRECTORY_FILE;
761        let response = self.connection.create(&create_request).await?;
762        let file_id = response.file_id;
763        let metadata = self.stat_by_id(file_id).await?;
764        self.connection
765            .close(&CloseRequest { flags: 0, file_id })
766            .await?;
767        Ok(metadata)
768    }
769
770    /// Queries file metadata on the current tree.
771    ///
772    /// This is an ergonomic alias for [`Share::stat`] for callers that prefer
773    /// a filesystem-style naming convention.
774    pub async fn metadata(&mut self, path: &str) -> Result<FileMetadata, CoreError> {
775        self.stat(path).await
776    }
777
778    /// Enumerates one directory on the current tree and returns the visible entries.
779    ///
780    /// The returned list filters out the `.` and `..` placeholders so embedders
781    /// get the entries they usually expect from a high-level client facade.
782    pub async fn list(&mut self, path: &str) -> Result<Vec<DirectoryEntry>, CoreError> {
783        let normalized_path = normalize_share_path(path)?;
784        let mut create_request = CreateRequest::from_path(&normalized_path);
785        create_request.desired_access = FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | SYNCHRONIZE;
786        create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
787        create_request.create_disposition = CreateDisposition::Open;
788        create_request.create_options = CreateOptions::DIRECTORY_FILE;
789        let response = self.connection.create(&create_request).await?;
790        let file_id = response.file_id;
791
792        let list_result = async {
793            let mut request =
794                QueryDirectoryRequest::for_pattern(file_id, "*", DIRECTORY_QUERY_BUFFER_SIZE);
795            let mut entries = Vec::new();
796
797            loop {
798                let response = self.connection.query_directory(&request).await?;
799                let batch = response
800                    .directory_entries()
801                    .map_err(CoreError::from)?
802                    .into_iter()
803                    .filter(|entry| entry.file_name != "." && entry.file_name != "..")
804                    .map(directory_entry_from_info)
805                    .collect::<Vec<_>>();
806                if batch.is_empty() {
807                    break;
808                }
809                entries.extend(batch);
810                request.flags = QueryDirectoryFlags::empty();
811                request.file_name.clear();
812            }
813
814            Ok(entries)
815        }
816        .await;
817
818        let close_result = self
819            .connection
820            .close(&CloseRequest { flags: 0, file_id })
821            .await;
822        match (list_result, close_result) {
823            (Ok(entries), Ok(_)) => Ok(entries),
824            (Err(error), _) => Err(error),
825            (Ok(_), Err(error)) => Err(error),
826        }
827    }
828
829    /// Enumerates one directory on the current tree.
830    ///
831    /// This is an alias for [`Share::list`] that matches the common filesystem
832    /// naming convention used by embedders.
833    pub async fn read_dir(&mut self, path: &str) -> Result<Vec<DirectoryEntry>, CoreError> {
834        self.list(path).await
835    }
836
837    /// Creates one directory on the current tree.
838    pub async fn create_dir(&mut self, path: &str) -> Result<(), CoreError> {
839        let normalized_path = normalize_share_path(path)?;
840        let mut create_request = CreateRequest::from_path(&normalized_path);
841        create_request.desired_access = FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE;
842        create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
843        create_request.file_attributes = FileAttributes::DIRECTORY;
844        create_request.create_disposition = CreateDisposition::Create;
845        create_request.create_options = CreateOptions::DIRECTORY_FILE;
846        let response = self.connection.create(&create_request).await?;
847        let file_id = response.file_id;
848        self.connection
849            .close(&CloseRequest { flags: 0, file_id })
850            .await?;
851        Ok(())
852    }
853
854    /// Creates one directory on the current tree.
855    ///
856    /// This is an alias for [`Share::create_dir`] that matches common shell
857    /// naming.
858    pub async fn mkdir(&mut self, path: &str) -> Result<(), CoreError> {
859        self.create_dir(path).await
860    }
861
862    /// Renames or moves one file-system entry within the current tree.
863    pub async fn rename(&mut self, source: &str, destination: &str) -> Result<(), CoreError> {
864        let normalized_source = normalize_share_path(source)?;
865        let normalized_destination = normalize_share_path(destination)?;
866
867        let mut create_request = CreateRequest::from_path(&normalized_source);
868        create_request.desired_access =
869            DELETE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE;
870        create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
871        create_request.create_disposition = CreateDisposition::Open;
872        let response = self.connection.create(&create_request).await?;
873        let file_id = response.file_id;
874
875        let rename_result = self
876            .connection
877            .set_info(&SetInfoRequest::for_file_info(
878                file_id,
879                FileInfoClass::RenameInformation,
880                RenameInformation::from_path(&normalized_destination, false).encode(),
881            ))
882            .await;
883        let close_result = self
884            .connection
885            .close(&CloseRequest { flags: 0, file_id })
886            .await;
887        match (rename_result, close_result) {
888            (Ok(_), Ok(_)) => Ok(()),
889            (Err(error), _) => Err(error),
890            (Ok(_), Err(error)) => Err(error),
891        }
892    }
893
894    /// Removes a file from the current tree by marking it delete-pending and closing it.
895    pub async fn remove(&mut self, path: &str) -> Result<(), CoreError> {
896        let normalized_path = normalize_share_path(path)?;
897        let mut create_request = CreateRequest::from_path(&normalized_path);
898        create_request.desired_access =
899            DELETE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE;
900        create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
901        create_request.create_disposition = CreateDisposition::Open;
902        create_request.create_options = CreateOptions::NON_DIRECTORY_FILE;
903        let response = self.connection.create(&create_request).await?;
904        let file_id = response.file_id;
905        self.connection
906            .set_info(&SetInfoRequest::for_file_info(
907                file_id,
908                FileInfoClass::DispositionInformation,
909                DispositionInformation {
910                    delete_pending: true,
911                }
912                .encode(),
913            ))
914            .await?;
915        self.connection
916            .close(&CloseRequest { flags: 0, file_id })
917            .await?;
918        Ok(())
919    }
920
921    /// Opens an existing file on the current tree for read access.
922    pub async fn open_reader(self, path: &str) -> Result<File<T>, CoreError> {
923        self.open(path, OpenOptions::new().read(true)).await
924    }
925
926    /// Opens a file on the current tree for write access, creating it when absent
927    /// and truncating it before writing.
928    pub async fn open_writer(self, path: &str) -> Result<File<T>, CoreError> {
929        self.open(
930            path,
931            OpenOptions::new().write(true).create(true).truncate(true),
932        )
933        .await
934    }
935
936    /// Disconnects the tree and returns to an authenticated session wrapper.
937    pub async fn disconnect(self) -> Result<Session<T>, CoreError> {
938        let connection = self.connection.tree_disconnect().await?;
939        Ok(Session {
940            server: self.server,
941            connection,
942        })
943    }
944
945    /// Disconnects the tree and logs off the SMB session.
946    pub async fn logoff(self) -> Result<(), CoreError> {
947        self.disconnect().await?.logoff().await
948    }
949
950    async fn stat_by_id(&mut self, file_id: FileId) -> Result<FileMetadata, CoreError> {
951        let basic = self
952            .connection
953            .query_info(&QueryInfoRequest::for_file_info(
954                file_id,
955                FileInfoClass::BasicInformation,
956            ))
957            .await?;
958        let basic = FileBasicInformation::decode(&basic.output_buffer).map_err(CoreError::from)?;
959
960        let standard = self
961            .connection
962            .query_info(&QueryInfoRequest::for_file_info(
963                file_id,
964                FileInfoClass::StandardInformation,
965            ))
966            .await?;
967        let standard =
968            FileStandardInformation::decode(&standard.output_buffer).map_err(CoreError::from)?;
969
970        Ok(metadata_from_info(basic, standard))
971    }
972}
973
974/// High-level open options for the embedded client facade.
975#[derive(Debug, Clone, PartialEq, Eq)]
976pub struct OpenOptions {
977    read: bool,
978    write: bool,
979    create: bool,
980    create_new: bool,
981    truncate: bool,
982    durable: Option<DurableOpenOptions>,
983    resilient_timeout: Option<u32>,
984}
985
986impl OpenOptions {
987    /// Builds a default empty option set.
988    #[must_use]
989    pub fn new() -> Self {
990        Self::default()
991    }
992
993    /// Enables or disables read access.
994    #[must_use]
995    pub fn read(mut self, read: bool) -> Self {
996        self.read = read;
997        self
998    }
999
1000    /// Enables or disables write access.
1001    #[must_use]
1002    pub fn write(mut self, write: bool) -> Self {
1003        self.write = write;
1004        self
1005    }
1006
1007    /// Creates the file if it does not exist.
1008    #[must_use]
1009    pub fn create(mut self, create: bool) -> Self {
1010        self.create = create;
1011        self
1012    }
1013
1014    /// Requires that the file be created and fail if it already exists.
1015    #[must_use]
1016    pub fn create_new(mut self, create_new: bool) -> Self {
1017        self.create_new = create_new;
1018        self
1019    }
1020
1021    /// Truncates the file when it is opened.
1022    #[must_use]
1023    pub fn truncate(mut self, truncate: bool) -> Self {
1024        self.truncate = truncate;
1025        self
1026    }
1027
1028    /// Requests a durable handle for the opened file.
1029    #[must_use]
1030    pub fn durable(mut self, durable: DurableOpenOptions) -> Self {
1031        self.durable = Some(durable);
1032        self
1033    }
1034
1035    /// Requests handle resiliency for the opened file.
1036    #[must_use]
1037    pub fn resilient(mut self, timeout: u32) -> Self {
1038        self.resilient_timeout = Some(timeout);
1039        self
1040    }
1041
1042    fn to_create_request(&self, path: &str) -> Result<CreateRequest, CoreError> {
1043        if !self.read && !self.write {
1044            return Err(CoreError::InvalidInput(
1045                "open options must request read and/or write access",
1046            ));
1047        }
1048        if (self.truncate || self.create || self.create_new) && !self.write {
1049            return Err(CoreError::InvalidInput(
1050                "create and truncate operations require write access",
1051            ));
1052        }
1053
1054        let mut request = CreateRequest::from_path(path);
1055        request.desired_access = desired_access_mask(self);
1056        request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
1057        request.file_attributes = FileAttributes::NORMAL;
1058        request.create_options = CreateOptions::NON_DIRECTORY_FILE;
1059        request.create_disposition = create_disposition(self);
1060        Ok(request)
1061    }
1062
1063    fn durable_options(&self, dialect: Dialect) -> Option<DurableOpenOptions> {
1064        self.durable.clone().map(|durable| {
1065            if dialect_supports_durable_v2(dialect) && durable.create_guid.is_none() {
1066                durable.with_create_guid(random())
1067            } else {
1068                durable
1069            }
1070        })
1071    }
1072}
1073
1074impl Default for OpenOptions {
1075    fn default() -> Self {
1076        Self {
1077            read: false,
1078            write: false,
1079            create: false,
1080            create_new: false,
1081            truncate: false,
1082            durable: None,
1083            resilient_timeout: None,
1084        }
1085    }
1086}
1087
1088/// High-level metadata for an SMB object.
1089#[derive(Debug, Clone, PartialEq, Eq)]
1090pub struct FileMetadata {
1091    /// Logical size of the object in bytes.
1092    pub size: u64,
1093    /// Allocated size of the object in bytes.
1094    pub allocation_size: u64,
1095    /// File attributes.
1096    pub attributes: FileAttributes,
1097    /// Creation time.
1098    pub created: Option<SystemTime>,
1099    /// Last access time.
1100    pub accessed: Option<SystemTime>,
1101    /// Last write time.
1102    pub written: Option<SystemTime>,
1103    /// Change time.
1104    pub changed: Option<SystemTime>,
1105    /// Whether the object is pending deletion.
1106    pub delete_pending: bool,
1107}
1108
1109impl FileMetadata {
1110    /// Returns true when the object is a directory.
1111    #[must_use]
1112    pub fn is_directory(&self) -> bool {
1113        self.attributes.contains(FileAttributes::DIRECTORY)
1114    }
1115
1116    /// Returns true when the object is a regular file.
1117    #[must_use]
1118    pub fn is_file(&self) -> bool {
1119        !self.is_directory()
1120    }
1121}
1122
1123/// One directory entry returned by the high-level share facade.
1124#[derive(Debug, Clone, PartialEq, Eq)]
1125pub struct DirectoryEntry {
1126    /// File name relative to the queried directory.
1127    pub name: String,
1128    /// Server-provided resume index for the entry.
1129    pub file_index: u32,
1130    /// High-level metadata derived from the SMB directory record.
1131    pub metadata: FileMetadata,
1132}
1133
1134impl DirectoryEntry {
1135    /// Returns true when the entry is a directory.
1136    #[must_use]
1137    pub fn is_directory(&self) -> bool {
1138        self.metadata.is_directory()
1139    }
1140
1141    /// Returns true when the entry is a regular file.
1142    #[must_use]
1143    pub fn is_file(&self) -> bool {
1144        self.metadata.is_file()
1145    }
1146}
1147
1148/// One open file handle on a tree-connected share.
1149#[derive(Debug)]
1150pub struct File<T = TokioTcpTransport> {
1151    share: Share<T>,
1152    path: String,
1153    file_id: FileId,
1154    durable_handle: Option<DurableHandle>,
1155    resilient_handle: Option<ResilientHandle>,
1156}
1157
1158impl<T> File<T>
1159where
1160    T: SmbTransport + Send,
1161{
1162    /// Returns the file path relative to the connected share.
1163    #[must_use]
1164    pub fn path(&self) -> &str {
1165        &self.path
1166    }
1167
1168    /// Returns the active SMB file identifier.
1169    #[must_use]
1170    pub fn file_id(&self) -> FileId {
1171        self.file_id
1172    }
1173
1174    /// Returns the durable reconnect state captured for this file, if requested.
1175    #[must_use]
1176    pub fn durable_handle(&self) -> Option<&DurableHandle> {
1177        self.durable_handle.as_ref()
1178    }
1179
1180    /// Returns the resiliency state captured for this file, if requested.
1181    #[must_use]
1182    pub fn resilient_handle(&self) -> Option<ResilientHandle> {
1183        self.resilient_handle
1184    }
1185
1186    /// Returns the wrapped tree-connected connection.
1187    #[must_use]
1188    pub fn connection(&self) -> &Connection<T, TreeConnected> {
1189        self.share.connection()
1190    }
1191
1192    /// Returns a mutable reference to the wrapped tree-connected connection.
1193    #[must_use]
1194    pub fn connection_mut(&mut self) -> &mut Connection<T, TreeConnected> {
1195        self.share.connection_mut()
1196    }
1197
1198    /// Reads the full contents of the open file.
1199    pub async fn read_all(&mut self) -> Result<Vec<u8>, CoreError> {
1200        let metadata = self.stat().await?;
1201        let mut output = Vec::with_capacity(usize::try_from(metadata.size).unwrap_or(0));
1202        let mut offset = 0u64;
1203
1204        while offset < metadata.size {
1205            let remaining = metadata.size - offset;
1206            let chunk_len = remaining.min(MAX_IO_CHUNK_SIZE as u64) as u32;
1207            let response = self
1208                .share
1209                .connection
1210                .read(&ReadRequest::for_file(self.file_id, offset, chunk_len))
1211                .await?;
1212            if response.data.is_empty() {
1213                break;
1214            }
1215            offset = offset.saturating_add(response.data.len() as u64);
1216            output.extend_from_slice(&response.data);
1217        }
1218
1219        Ok(output)
1220    }
1221
1222    /// Reads the full contents of the open file.
1223    ///
1224    /// This is an alias for [`File::read_all`] that matches the standard async
1225    /// I/O naming convention used by Rust callers.
1226    pub async fn read_to_end(&mut self) -> Result<Vec<u8>, CoreError> {
1227        self.read_all().await
1228    }
1229
1230    /// Writes the full provided buffer to the open file starting at offset zero.
1231    pub async fn write_all(&mut self, data: &[u8]) -> Result<(), CoreError> {
1232        let mut offset = 0u64;
1233        while (offset as usize) < data.len() {
1234            let chunk_end = (offset as usize + MAX_IO_CHUNK_SIZE).min(data.len());
1235            self.share
1236                .connection
1237                .write(&WriteRequest::for_file(
1238                    self.file_id,
1239                    offset,
1240                    data[offset as usize..chunk_end].to_vec(),
1241                ))
1242                .await?;
1243            offset = chunk_end as u64;
1244        }
1245        Ok(())
1246    }
1247
1248    /// Flushes the open file handle.
1249    pub async fn flush(&mut self) -> Result<(), CoreError> {
1250        let _ = self
1251            .share
1252            .connection
1253            .flush(&FlushRequest::for_file(self.file_id))
1254            .await?;
1255        Ok(())
1256    }
1257
1258    /// Flushes all buffered SMB state for the open file handle.
1259    ///
1260    /// This is an alias for [`File::flush`] that matches common filesystem
1261    /// terminology used by embedders.
1262    pub async fn sync_all(&mut self) -> Result<(), CoreError> {
1263        self.flush().await
1264    }
1265
1266    /// Queries metadata for the open file handle.
1267    pub async fn stat(&mut self) -> Result<FileMetadata, CoreError> {
1268        self.share.stat_by_id(self.file_id).await
1269    }
1270
1271    /// Requests handle resiliency for the current file and stores the result for future reconnects.
1272    pub async fn request_resiliency(&mut self, timeout: u32) -> Result<ResilientHandle, CoreError> {
1273        let resilient = self
1274            .share
1275            .connection
1276            .request_resiliency(self.file_id, timeout)
1277            .await?;
1278        self.resilient_handle = Some(resilient);
1279        if let Some(durable) = self.durable_handle.take() {
1280            self.durable_handle = Some(durable.with_resilient_timeout(timeout));
1281        }
1282        Ok(resilient)
1283    }
1284
1285    /// Closes the file and returns the tree-connected share wrapper.
1286    pub async fn close(mut self) -> Result<Share<T>, CoreError> {
1287        self.share
1288            .connection
1289            .close(&CloseRequest {
1290                flags: 0,
1291                file_id: self.file_id,
1292            })
1293            .await?;
1294        Ok(self.share)
1295    }
1296
1297    /// Consumes the file wrapper and returns the share wrapper plus low-level file state.
1298    #[must_use]
1299    pub fn into_parts(
1300        self,
1301    ) -> (
1302        Share<T>,
1303        FileId,
1304        Option<DurableHandle>,
1305        Option<ResilientHandle>,
1306    ) {
1307        (
1308            self.share,
1309            self.file_id,
1310            self.durable_handle,
1311            self.resilient_handle,
1312        )
1313    }
1314}
1315
1316fn normalize_share_name(share: &str) -> Result<String, CoreError> {
1317    let share = share.trim_matches(['\\', '/']);
1318    if share.is_empty() {
1319        return Err(CoreError::PathInvalid("share name must not be empty"));
1320    }
1321    if share.contains(['\\', '/', '\0']) {
1322        return Err(CoreError::PathInvalid(
1323            "share name must not contain separators or NUL bytes",
1324        ));
1325    }
1326    Ok(share.to_owned())
1327}
1328
1329fn normalize_share_path(path: &str) -> Result<String, CoreError> {
1330    if path.contains('\0') {
1331        return Err(CoreError::PathInvalid("path must not contain NUL bytes"));
1332    }
1333    if matches!(path, "\\" | "/") {
1334        return Ok("\\".to_string());
1335    }
1336
1337    let normalized = path
1338        .split(['\\', '/'])
1339        .filter(|segment| !segment.is_empty())
1340        .collect::<Vec<_>>()
1341        .join("\\");
1342    if normalized.is_empty() {
1343        return Err(CoreError::PathInvalid("path must not be empty"));
1344    }
1345    Ok(normalized)
1346}
1347
1348fn normalize_pipe_name(pipe_name: &str) -> Result<String, CoreError> {
1349    if pipe_name.contains('\0') {
1350        return Err(CoreError::PathInvalid(
1351            "pipe name must not contain NUL bytes",
1352        ));
1353    }
1354
1355    let normalized = pipe_name.trim().replace('/', "\\");
1356    let trimmed = normalized.trim_matches('\\');
1357    if trimmed.is_empty() {
1358        return Err(CoreError::PathInvalid("pipe name must not be empty"));
1359    }
1360
1361    let trimmed = if trimmed.len() > 5 && trimmed[..5].eq_ignore_ascii_case("pipe\\") {
1362        trimmed[5..].trim_start_matches('\\')
1363    } else {
1364        trimmed
1365    };
1366    if trimmed.is_empty() {
1367        return Err(CoreError::PathInvalid("pipe name must not be empty"));
1368    }
1369
1370    Ok(trimmed.to_owned())
1371}
1372
1373fn desired_access_mask(options: &OpenOptions) -> u32 {
1374    let mut desired_access = READ_CONTROL | SYNCHRONIZE;
1375    if options.read {
1376        desired_access |= FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES;
1377    }
1378    if options.write {
1379        desired_access |=
1380            FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
1381    }
1382    desired_access
1383}
1384
1385fn create_disposition(options: &OpenOptions) -> CreateDisposition {
1386    if options.create_new {
1387        CreateDisposition::Create
1388    } else if options.create && options.truncate {
1389        CreateDisposition::OverwriteIf
1390    } else if options.create {
1391        CreateDisposition::OpenIf
1392    } else if options.truncate {
1393        CreateDisposition::Overwrite
1394    } else {
1395        CreateDisposition::Open
1396    }
1397}
1398
1399fn dialect_supports_durable_v2(dialect: Dialect) -> bool {
1400    matches!(dialect, Dialect::Smb300 | Dialect::Smb302 | Dialect::Smb311)
1401}
1402
1403fn metadata_from_info(
1404    basic: FileBasicInformation,
1405    standard: FileStandardInformation,
1406) -> FileMetadata {
1407    let mut attributes = basic.file_attributes;
1408    if standard.directory {
1409        attributes |= FileAttributes::DIRECTORY;
1410    }
1411
1412    FileMetadata {
1413        size: standard.end_of_file,
1414        allocation_size: standard.allocation_size,
1415        attributes,
1416        created: system_time_from_windows_ticks(basic.creation_time),
1417        accessed: system_time_from_windows_ticks(basic.last_access_time),
1418        written: system_time_from_windows_ticks(basic.last_write_time),
1419        changed: system_time_from_windows_ticks(basic.change_time),
1420        delete_pending: standard.delete_pending,
1421    }
1422}
1423
1424fn directory_entry_from_info(entry: DirectoryInformationEntry) -> DirectoryEntry {
1425    DirectoryEntry {
1426        name: entry.file_name,
1427        file_index: entry.file_index,
1428        metadata: FileMetadata {
1429            size: entry.end_of_file,
1430            allocation_size: entry.allocation_size,
1431            attributes: entry.file_attributes,
1432            created: system_time_from_windows_ticks(entry.creation_time),
1433            accessed: system_time_from_windows_ticks(entry.last_access_time),
1434            written: system_time_from_windows_ticks(entry.last_write_time),
1435            changed: system_time_from_windows_ticks(entry.change_time),
1436            delete_pending: false,
1437        },
1438    }
1439}
1440
1441fn system_time_from_windows_ticks(value: u64) -> Option<SystemTime> {
1442    if value == 0 {
1443        return None;
1444    }
1445
1446    let unix_ticks = value.checked_sub(SEC_TO_UNIX_EPOCH * WINDOWS_TICK)?;
1447    Some(UNIX_EPOCH + Duration::from_nanos(unix_ticks.saturating_mul(100)))
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452    use std::collections::VecDeque;
1453
1454    use async_trait::async_trait;
1455    use smolder_proto::smb::netbios::SessionMessage;
1456    use smolder_proto::smb::smb2::{
1457        CloseResponse, Command, CreateDisposition, CreateResponse, Dialect, FileAttributes, FileId,
1458        FlushResponse, GlobalCapabilities, Header, MessageId, NegotiateRequest, NegotiateResponse,
1459        OplockLevel, QueryDirectoryResponse, QueryInfoResponse, SessionFlags, SessionSetupResponse,
1460        ShareFlags, ShareType, SigningMode, TreeCapabilities, TreeConnectRequest,
1461        TreeConnectResponse, TreeId, WriteResponse,
1462    };
1463    use smolder_proto::smb::status::NtStatus;
1464
1465    use crate::auth::NtlmAuthenticator;
1466    use crate::auth::NtlmCredentials;
1467    #[cfg(feature = "kerberos-api")]
1468    use crate::auth::{KerberosCredentials, KerberosTarget};
1469    use crate::client::Connection;
1470    use crate::transport::Transport;
1471    use crate::transport::{TransportProtocol, TransportTarget};
1472
1473    use super::{
1474        normalize_pipe_name, normalize_share_name, normalize_share_path, Client, ClientBuilder,
1475        FileMetadata, OpenOptions, Share,
1476    };
1477
1478    #[derive(Debug)]
1479    struct ScriptedTransport {
1480        reads: VecDeque<Vec<u8>>,
1481        writes: Vec<Vec<u8>>,
1482    }
1483
1484    impl ScriptedTransport {
1485        fn new(reads: Vec<Vec<u8>>) -> Self {
1486            Self {
1487                reads: reads.into(),
1488                writes: Vec::new(),
1489            }
1490        }
1491    }
1492
1493    #[async_trait]
1494    impl Transport for ScriptedTransport {
1495        async fn send(&mut self, frame: &[u8]) -> std::io::Result<()> {
1496            self.writes.push(frame.to_vec());
1497            Ok(())
1498        }
1499
1500        async fn recv(&mut self) -> std::io::Result<Vec<u8>> {
1501            self.reads.pop_front().ok_or_else(|| {
1502                std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "no scripted response")
1503            })
1504        }
1505    }
1506
1507    fn response_frame(
1508        command: Command,
1509        status: u32,
1510        message_id: u64,
1511        session_id: u64,
1512        tree_id: u32,
1513        body: Vec<u8>,
1514    ) -> Vec<u8> {
1515        let mut header = Header::new(command, MessageId(message_id));
1516        header.status = status;
1517        header.credit_request_response = 1;
1518        header.session_id = smolder_proto::smb::smb2::SessionId(session_id);
1519        header.tree_id = TreeId(tree_id);
1520
1521        let mut packet = header.encode();
1522        packet.extend_from_slice(&body);
1523        SessionMessage::new(packet)
1524            .encode()
1525            .expect("response should frame")
1526    }
1527
1528    fn directory_entries_buffer(entries: &[(u32, FileAttributes, u64, &str)]) -> Vec<u8> {
1529        let mut buffer = Vec::new();
1530
1531        for (index, (file_index, attributes, size, name)) in entries.iter().enumerate() {
1532            let name_bytes = name
1533                .encode_utf16()
1534                .flat_map(|unit| unit.to_le_bytes())
1535                .collect::<Vec<_>>();
1536            let entry_len = (64 + name_bytes.len() + 7) & !7;
1537            let next_entry_offset = if index + 1 == entries.len() {
1538                0
1539            } else {
1540                entry_len as u32
1541            };
1542
1543            buffer.extend_from_slice(&next_entry_offset.to_le_bytes());
1544            buffer.extend_from_slice(&file_index.to_le_bytes());
1545            buffer.extend_from_slice(&1_u64.to_le_bytes());
1546            buffer.extend_from_slice(&2_u64.to_le_bytes());
1547            buffer.extend_from_slice(&3_u64.to_le_bytes());
1548            buffer.extend_from_slice(&4_u64.to_le_bytes());
1549            buffer.extend_from_slice(&size.to_le_bytes());
1550            buffer.extend_from_slice(&size.to_le_bytes());
1551            buffer.extend_from_slice(&attributes.bits().to_le_bytes());
1552            buffer.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1553            buffer.extend_from_slice(&name_bytes);
1554            let padding = entry_len - 64 - name_bytes.len();
1555            buffer.resize(buffer.len() + padding, 0);
1556        }
1557
1558        buffer
1559    }
1560
1561    async fn build_share(reads: Vec<Vec<u8>>) -> Share<ScriptedTransport> {
1562        let negotiate_response = NegotiateResponse {
1563            security_mode: SigningMode::ENABLED,
1564            dialect_revision: Dialect::Smb302,
1565            negotiate_contexts: Vec::new(),
1566            server_guid: *b"server-guid-0001",
1567            capabilities: GlobalCapabilities::LARGE_MTU,
1568            max_transact_size: 65_536,
1569            max_read_size: 65_536,
1570            max_write_size: 65_536,
1571            system_time: 1,
1572            server_start_time: 1,
1573            security_buffer: Vec::new(),
1574        };
1575        let session_response = SessionSetupResponse {
1576            session_flags: SessionFlags::empty(),
1577            security_buffer: Vec::new(),
1578        };
1579        let tree_response = TreeConnectResponse {
1580            share_type: ShareType::Disk,
1581            share_flags: ShareFlags::empty(),
1582            capabilities: TreeCapabilities::empty(),
1583            maximal_access: 0x0012_019f,
1584        };
1585
1586        let mut scripted_reads = vec![
1587            response_frame(
1588                Command::Negotiate,
1589                NtStatus::SUCCESS.to_u32(),
1590                0,
1591                0,
1592                0,
1593                negotiate_response.encode(),
1594            ),
1595            response_frame(
1596                Command::SessionSetup,
1597                NtStatus::SUCCESS.to_u32(),
1598                1,
1599                11,
1600                0,
1601                session_response.encode(),
1602            ),
1603            response_frame(
1604                Command::TreeConnect,
1605                NtStatus::SUCCESS.to_u32(),
1606                2,
1607                11,
1608                7,
1609                tree_response.encode(),
1610            ),
1611        ];
1612        scripted_reads.extend(reads);
1613
1614        let transport = ScriptedTransport::new(scripted_reads);
1615        let negotiate_request = NegotiateRequest {
1616            security_mode: SigningMode::ENABLED,
1617            capabilities: GlobalCapabilities::LARGE_MTU,
1618            client_guid: *b"client-guid-0001",
1619            dialects: vec![Dialect::Smb210, Dialect::Smb302],
1620            negotiate_contexts: Vec::new(),
1621        };
1622        let connection = Connection::new(transport)
1623            .negotiate(&negotiate_request)
1624            .await
1625            .expect("negotiate should succeed");
1626        let mut auth = NtlmAuthenticator::new(NtlmCredentials::new("user", "pass"));
1627        let connection = connection
1628            .authenticate(&mut auth)
1629            .await
1630            .expect("authenticate should succeed");
1631        let connection = connection
1632            .tree_connect(&TreeConnectRequest::from_unc(r"\\server\share"))
1633            .await
1634            .expect("tree connect should succeed");
1635
1636        Share {
1637            server: "server".to_owned(),
1638            name: "share".to_owned(),
1639            connection,
1640        }
1641    }
1642
1643    #[test]
1644    fn builder_requires_credentials() {
1645        let error = Client::builder("server")
1646            .build()
1647            .expect_err("builder should reject missing credentials");
1648        assert!(matches!(
1649            error,
1650            crate::error::CoreError::InvalidInput(
1651                "client builder requires NTLM or Kerberos credentials"
1652            )
1653        ));
1654    }
1655
1656    #[test]
1657    fn ntlm_builder_populates_session_config() {
1658        let client = ClientBuilder::new("server")
1659            .with_port(1445)
1660            .with_signing_mode(SigningMode::REQUIRED)
1661            .with_capabilities(GlobalCapabilities::ENCRYPTION)
1662            .with_dialects(vec![Dialect::Smb302])
1663            .with_client_guid(*b"0123456789abcdef")
1664            .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1665            .build()
1666            .expect("builder should produce a client");
1667
1668        let config = client.session_config();
1669        assert_eq!(client.server(), "server");
1670        assert_eq!(client.port(), 1445);
1671        assert_eq!(config.server(), "server");
1672        assert_eq!(config.port(), 1445);
1673        assert_eq!(config.signing_mode(), SigningMode::REQUIRED);
1674        assert_eq!(config.capabilities(), GlobalCapabilities::ENCRYPTION);
1675        assert_eq!(config.dialects(), &[Dialect::Smb302]);
1676        assert_eq!(config.client_guid(), b"0123456789abcdef");
1677    }
1678
1679    #[test]
1680    fn builder_can_override_transport_target() {
1681        let client = ClientBuilder::new("server")
1682            .with_transport_target(
1683                TransportTarget::quic("edge.lab.example")
1684                    .with_connect_host("127.0.0.1")
1685                    .with_tls_server_name("gateway.lab.example")
1686                    .with_port(8443),
1687            )
1688            .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1689            .build()
1690            .expect("builder should produce a client");
1691
1692        assert_eq!(client.server(), "edge.lab.example");
1693        assert_eq!(client.connect_host(), "127.0.0.1");
1694        assert_eq!(client.tls_server_name(), "gateway.lab.example");
1695        assert_eq!(client.port(), 8443);
1696        assert_eq!(client.transport_protocol(), TransportProtocol::Quic);
1697        assert_eq!(
1698            client.transport_target(),
1699            &TransportTarget::quic("edge.lab.example")
1700                .with_connect_host("127.0.0.1")
1701                .with_tls_server_name("gateway.lab.example")
1702                .with_port(8443)
1703        );
1704    }
1705
1706    #[test]
1707    fn builder_can_override_connect_host_without_replacing_target() {
1708        let client = ClientBuilder::new("files.lab.example")
1709            .with_connect_host("127.0.0.1")
1710            .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1711            .build()
1712            .expect("builder should produce a client");
1713
1714        assert_eq!(client.server(), "files.lab.example");
1715        assert_eq!(client.connect_host(), "127.0.0.1");
1716        assert_eq!(client.transport_target().server(), "files.lab.example");
1717        assert_eq!(client.transport_protocol(), TransportProtocol::Tcp);
1718    }
1719
1720    #[cfg(feature = "kerberos-api")]
1721    #[test]
1722    fn kerberos_builder_produces_client() {
1723        let credentials = {
1724            #[cfg(feature = "kerberos-sspi")]
1725            {
1726                KerberosCredentials::new("user@LAB.EXAMPLE", "pass")
1727            }
1728            #[cfg(all(unix, feature = "kerberos-gssapi", not(feature = "kerberos-sspi")))]
1729            {
1730                KerberosCredentials::from_ticket_cache("user@LAB.EXAMPLE")
1731            }
1732        };
1733        let target = KerberosTarget::for_smb_host("server.lab.example");
1734        let client = ClientBuilder::new("server.lab.example")
1735            .with_kerberos_credentials(credentials, target)
1736            .build()
1737            .expect("builder should produce a client");
1738        assert_eq!(client.server(), "server.lab.example");
1739        assert_eq!(client.port(), 445);
1740    }
1741
1742    #[test]
1743    fn normalize_share_name_rejects_invalid_values() {
1744        assert_eq!(
1745            normalize_share_name(r"\\IPC$\\").expect("share should normalize"),
1746            "IPC$"
1747        );
1748        assert!(normalize_share_name("").is_err());
1749        assert!(normalize_share_name("share/path").is_err());
1750    }
1751
1752    #[test]
1753    fn open_options_map_to_expected_create_request() {
1754        let request = OpenOptions::new()
1755            .write(true)
1756            .create(true)
1757            .truncate(true)
1758            .to_create_request("docs\\report.txt")
1759            .expect("request should build");
1760        assert_eq!(request.create_disposition, CreateDisposition::OverwriteIf);
1761    }
1762
1763    #[test]
1764    fn normalize_share_path_rejects_invalid_values() {
1765        assert_eq!(
1766            normalize_share_path(r"/docs//nested\file.txt/").expect("path should normalize"),
1767            "docs\\nested\\file.txt"
1768        );
1769        assert!(normalize_share_path("").is_err());
1770        assert!(normalize_share_path("\0bad").is_err());
1771    }
1772
1773    #[test]
1774    fn normalize_pipe_name_rejects_invalid_values() {
1775        assert_eq!(
1776            normalize_pipe_name("srvsvc").expect("pipe should normalize"),
1777            "srvsvc"
1778        );
1779        assert_eq!(
1780            normalize_pipe_name(r"\\PIPE\\srvsvc").expect("pipe should normalize"),
1781            "srvsvc"
1782        );
1783        assert!(normalize_pipe_name("").is_err());
1784        assert!(normalize_pipe_name("\0bad").is_err());
1785    }
1786
1787    #[tokio::test]
1788    async fn share_read_queries_metadata_and_reads_contents() {
1789        let create_response = CreateResponse {
1790            oplock_level: OplockLevel::None,
1791            file_attributes: FileAttributes::ARCHIVE,
1792            allocation_size: 5,
1793            end_of_file: 5,
1794            file_id: FileId {
1795                persistent: 1,
1796                volatile: 2,
1797            },
1798            create_contexts: Vec::new(),
1799        };
1800        let basic = QueryInfoResponse {
1801            output_buffer: {
1802                let mut buffer = Vec::new();
1803                buffer.extend_from_slice(&1u64.to_le_bytes());
1804                buffer.extend_from_slice(&2u64.to_le_bytes());
1805                buffer.extend_from_slice(&3u64.to_le_bytes());
1806                buffer.extend_from_slice(&4u64.to_le_bytes());
1807                buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
1808                buffer.extend_from_slice(&0u32.to_le_bytes());
1809                buffer
1810            },
1811        };
1812        let standard = QueryInfoResponse {
1813            output_buffer: {
1814                let mut buffer = Vec::new();
1815                buffer.extend_from_slice(&5u64.to_le_bytes());
1816                buffer.extend_from_slice(&5u64.to_le_bytes());
1817                buffer.extend_from_slice(&1u32.to_le_bytes());
1818                buffer.push(0);
1819                buffer.push(0);
1820                buffer.extend_from_slice(&0u16.to_le_bytes());
1821                buffer
1822            },
1823        };
1824        let read_response = smolder_proto::smb::smb2::ReadResponse {
1825            data_remaining: 0,
1826            flags: smolder_proto::smb::smb2::ReadResponseFlags::empty(),
1827            data: b"hello".to_vec(),
1828        };
1829
1830        let mut share = build_share(vec![
1831            response_frame(
1832                Command::Create,
1833                NtStatus::SUCCESS.to_u32(),
1834                3,
1835                11,
1836                7,
1837                create_response.encode(),
1838            ),
1839            response_frame(
1840                Command::QueryInfo,
1841                NtStatus::SUCCESS.to_u32(),
1842                4,
1843                11,
1844                7,
1845                basic.encode(),
1846            ),
1847            response_frame(
1848                Command::QueryInfo,
1849                NtStatus::SUCCESS.to_u32(),
1850                5,
1851                11,
1852                7,
1853                standard.encode(),
1854            ),
1855            response_frame(
1856                Command::Read,
1857                NtStatus::SUCCESS.to_u32(),
1858                6,
1859                11,
1860                7,
1861                read_response.encode(),
1862            ),
1863            response_frame(
1864                Command::Close,
1865                NtStatus::SUCCESS.to_u32(),
1866                7,
1867                11,
1868                7,
1869                CloseResponse {
1870                    flags: 0,
1871                    allocation_size: 5,
1872                    end_of_file: 5,
1873                    file_attributes: FileAttributes::ARCHIVE,
1874                }
1875                .encode(),
1876            ),
1877        ])
1878        .await;
1879
1880        let data = share.read("notes.txt").await.expect("read should succeed");
1881        assert_eq!(data, b"hello");
1882    }
1883
1884    #[tokio::test]
1885    async fn share_get_alias_reads_contents() {
1886        let create_response = CreateResponse {
1887            oplock_level: OplockLevel::None,
1888            file_attributes: FileAttributes::ARCHIVE,
1889            allocation_size: 5,
1890            end_of_file: 5,
1891            file_id: FileId {
1892                persistent: 1,
1893                volatile: 2,
1894            },
1895            create_contexts: Vec::new(),
1896        };
1897        let basic = QueryInfoResponse {
1898            output_buffer: {
1899                let mut buffer = Vec::new();
1900                buffer.extend_from_slice(&1u64.to_le_bytes());
1901                buffer.extend_from_slice(&2u64.to_le_bytes());
1902                buffer.extend_from_slice(&3u64.to_le_bytes());
1903                buffer.extend_from_slice(&4u64.to_le_bytes());
1904                buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
1905                buffer.extend_from_slice(&0u32.to_le_bytes());
1906                buffer
1907            },
1908        };
1909        let standard = QueryInfoResponse {
1910            output_buffer: {
1911                let mut buffer = Vec::new();
1912                buffer.extend_from_slice(&5u64.to_le_bytes());
1913                buffer.extend_from_slice(&5u64.to_le_bytes());
1914                buffer.extend_from_slice(&1u32.to_le_bytes());
1915                buffer.push(0);
1916                buffer.push(0);
1917                buffer.extend_from_slice(&0u16.to_le_bytes());
1918                buffer
1919            },
1920        };
1921        let read_response = smolder_proto::smb::smb2::ReadResponse {
1922            data_remaining: 0,
1923            flags: smolder_proto::smb::smb2::ReadResponseFlags::empty(),
1924            data: b"hello".to_vec(),
1925        };
1926
1927        let mut share = build_share(vec![
1928            response_frame(
1929                Command::Create,
1930                NtStatus::SUCCESS.to_u32(),
1931                3,
1932                11,
1933                7,
1934                create_response.encode(),
1935            ),
1936            response_frame(
1937                Command::QueryInfo,
1938                NtStatus::SUCCESS.to_u32(),
1939                4,
1940                11,
1941                7,
1942                basic.encode(),
1943            ),
1944            response_frame(
1945                Command::QueryInfo,
1946                NtStatus::SUCCESS.to_u32(),
1947                5,
1948                11,
1949                7,
1950                standard.encode(),
1951            ),
1952            response_frame(
1953                Command::Read,
1954                NtStatus::SUCCESS.to_u32(),
1955                6,
1956                11,
1957                7,
1958                read_response.encode(),
1959            ),
1960            response_frame(
1961                Command::Close,
1962                NtStatus::SUCCESS.to_u32(),
1963                7,
1964                11,
1965                7,
1966                CloseResponse {
1967                    flags: 0,
1968                    allocation_size: 5,
1969                    end_of_file: 5,
1970                    file_attributes: FileAttributes::ARCHIVE,
1971                }
1972                .encode(),
1973            ),
1974        ])
1975        .await;
1976
1977        let data = share.get("notes.txt").await.expect("get should succeed");
1978        assert_eq!(data, b"hello");
1979    }
1980
1981    #[tokio::test]
1982    async fn share_put_alias_writes_contents() {
1983        let create_response = CreateResponse {
1984            oplock_level: OplockLevel::None,
1985            file_attributes: FileAttributes::ARCHIVE,
1986            allocation_size: 5,
1987            end_of_file: 5,
1988            file_id: FileId {
1989                persistent: 1,
1990                volatile: 2,
1991            },
1992            create_contexts: Vec::new(),
1993        };
1994
1995        let mut share = build_share(vec![
1996            response_frame(
1997                Command::Create,
1998                NtStatus::SUCCESS.to_u32(),
1999                3,
2000                11,
2001                7,
2002                create_response.encode(),
2003            ),
2004            response_frame(
2005                Command::Write,
2006                NtStatus::SUCCESS.to_u32(),
2007                4,
2008                11,
2009                7,
2010                WriteResponse { count: 5 }.encode(),
2011            ),
2012            response_frame(
2013                Command::Flush,
2014                NtStatus::SUCCESS.to_u32(),
2015                5,
2016                11,
2017                7,
2018                FlushResponse.encode(),
2019            ),
2020            response_frame(
2021                Command::Close,
2022                NtStatus::SUCCESS.to_u32(),
2023                6,
2024                11,
2025                7,
2026                CloseResponse {
2027                    flags: 0,
2028                    allocation_size: 5,
2029                    end_of_file: 5,
2030                    file_attributes: FileAttributes::ARCHIVE,
2031                }
2032                .encode(),
2033            ),
2034        ])
2035        .await;
2036
2037        share
2038            .put("notes.txt", b"hello")
2039            .await
2040            .expect("put should succeed");
2041    }
2042
2043    #[tokio::test]
2044    async fn share_stat_decodes_basic_and_standard_info() {
2045        let create_response = CreateResponse {
2046            oplock_level: OplockLevel::None,
2047            file_attributes: FileAttributes::ARCHIVE,
2048            allocation_size: 7,
2049            end_of_file: 5,
2050            file_id: FileId {
2051                persistent: 1,
2052                volatile: 2,
2053            },
2054            create_contexts: Vec::new(),
2055        };
2056        let basic = QueryInfoResponse {
2057            output_buffer: {
2058                let mut buffer = Vec::new();
2059                buffer.extend_from_slice(&1u64.to_le_bytes());
2060                buffer.extend_from_slice(&2u64.to_le_bytes());
2061                buffer.extend_from_slice(&3u64.to_le_bytes());
2062                buffer.extend_from_slice(&4u64.to_le_bytes());
2063                buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
2064                buffer.extend_from_slice(&0u32.to_le_bytes());
2065                buffer
2066            },
2067        };
2068        let standard = QueryInfoResponse {
2069            output_buffer: {
2070                let mut buffer = Vec::new();
2071                buffer.extend_from_slice(&7u64.to_le_bytes());
2072                buffer.extend_from_slice(&5u64.to_le_bytes());
2073                buffer.extend_from_slice(&1u32.to_le_bytes());
2074                buffer.push(1);
2075                buffer.push(0);
2076                buffer.extend_from_slice(&0u16.to_le_bytes());
2077                buffer
2078            },
2079        };
2080
2081        let mut share = build_share(vec![
2082            response_frame(
2083                Command::Create,
2084                NtStatus::SUCCESS.to_u32(),
2085                3,
2086                11,
2087                7,
2088                create_response.encode(),
2089            ),
2090            response_frame(
2091                Command::QueryInfo,
2092                NtStatus::SUCCESS.to_u32(),
2093                4,
2094                11,
2095                7,
2096                basic.encode(),
2097            ),
2098            response_frame(
2099                Command::QueryInfo,
2100                NtStatus::SUCCESS.to_u32(),
2101                5,
2102                11,
2103                7,
2104                standard.encode(),
2105            ),
2106            response_frame(
2107                Command::Close,
2108                NtStatus::SUCCESS.to_u32(),
2109                6,
2110                11,
2111                7,
2112                CloseResponse {
2113                    flags: 0,
2114                    allocation_size: 7,
2115                    end_of_file: 5,
2116                    file_attributes: FileAttributes::ARCHIVE,
2117                }
2118                .encode(),
2119            ),
2120        ])
2121        .await;
2122
2123        let metadata: FileMetadata = share.stat("notes.txt").await.expect("stat should succeed");
2124        assert_eq!(metadata.size, 5);
2125        assert_eq!(metadata.allocation_size, 7);
2126        assert!(metadata.delete_pending);
2127        assert!(metadata.is_file());
2128    }
2129
2130    #[tokio::test]
2131    async fn share_list_decodes_directory_entries_and_filters_dot_entries() {
2132        let create_response = CreateResponse {
2133            oplock_level: OplockLevel::None,
2134            file_attributes: FileAttributes::DIRECTORY,
2135            allocation_size: 0,
2136            end_of_file: 0,
2137            file_id: FileId {
2138                persistent: 10,
2139                volatile: 20,
2140            },
2141            create_contexts: Vec::new(),
2142        };
2143        let first_page = QueryDirectoryResponse {
2144            output_buffer: directory_entries_buffer(&[
2145                (1, FileAttributes::DIRECTORY, 0, "."),
2146                (2, FileAttributes::ARCHIVE, 5, "notes.txt"),
2147            ]),
2148        };
2149        let second_page = QueryDirectoryResponse {
2150            output_buffer: directory_entries_buffer(&[
2151                (3, FileAttributes::DIRECTORY, 0, ".."),
2152                (4, FileAttributes::DIRECTORY, 0, "nested"),
2153            ]),
2154        };
2155
2156        let mut share = build_share(vec![
2157            response_frame(
2158                Command::Create,
2159                NtStatus::SUCCESS.to_u32(),
2160                3,
2161                11,
2162                7,
2163                create_response.encode(),
2164            ),
2165            response_frame(
2166                Command::QueryDirectory,
2167                NtStatus::SUCCESS.to_u32(),
2168                4,
2169                11,
2170                7,
2171                first_page.encode(),
2172            ),
2173            response_frame(
2174                Command::QueryDirectory,
2175                NtStatus::SUCCESS.to_u32(),
2176                5,
2177                11,
2178                7,
2179                second_page.encode(),
2180            ),
2181            response_frame(
2182                Command::QueryDirectory,
2183                NtStatus::NO_MORE_FILES.to_u32(),
2184                6,
2185                11,
2186                7,
2187                Vec::new(),
2188            ),
2189            response_frame(
2190                Command::Close,
2191                NtStatus::SUCCESS.to_u32(),
2192                7,
2193                11,
2194                7,
2195                CloseResponse {
2196                    flags: 0,
2197                    allocation_size: 0,
2198                    end_of_file: 0,
2199                    file_attributes: FileAttributes::DIRECTORY,
2200                }
2201                .encode(),
2202            ),
2203        ])
2204        .await;
2205
2206        let entries = share.list("\\").await.expect("list should succeed");
2207        assert_eq!(entries.len(), 2);
2208        assert_eq!(entries[0].name, "notes.txt");
2209        assert!(entries[0].is_file());
2210        assert_eq!(entries[0].metadata.size, 5);
2211        assert_eq!(entries[1].name, "nested");
2212        assert!(entries[1].is_directory());
2213    }
2214
2215    #[tokio::test]
2216    async fn share_create_dir_creates_and_closes_directory_handle() {
2217        let create_response = CreateResponse {
2218            oplock_level: OplockLevel::None,
2219            file_attributes: FileAttributes::DIRECTORY,
2220            allocation_size: 0,
2221            end_of_file: 0,
2222            file_id: FileId {
2223                persistent: 30,
2224                volatile: 40,
2225            },
2226            create_contexts: Vec::new(),
2227        };
2228
2229        let mut share = build_share(vec![
2230            response_frame(
2231                Command::Create,
2232                NtStatus::SUCCESS.to_u32(),
2233                3,
2234                11,
2235                7,
2236                create_response.encode(),
2237            ),
2238            response_frame(
2239                Command::Close,
2240                NtStatus::SUCCESS.to_u32(),
2241                4,
2242                11,
2243                7,
2244                CloseResponse {
2245                    flags: 0,
2246                    allocation_size: 0,
2247                    end_of_file: 0,
2248                    file_attributes: FileAttributes::DIRECTORY,
2249                }
2250                .encode(),
2251            ),
2252        ])
2253        .await;
2254
2255        share
2256            .create_dir("nested")
2257            .await
2258            .expect("create_dir should succeed");
2259    }
2260
2261    #[tokio::test]
2262    async fn share_rename_sets_rename_information_and_closes_handle() {
2263        let create_response = CreateResponse {
2264            oplock_level: OplockLevel::None,
2265            file_attributes: FileAttributes::ARCHIVE,
2266            allocation_size: 5,
2267            end_of_file: 5,
2268            file_id: FileId {
2269                persistent: 50,
2270                volatile: 60,
2271            },
2272            create_contexts: Vec::new(),
2273        };
2274
2275        let mut share = build_share(vec![
2276            response_frame(
2277                Command::Create,
2278                NtStatus::SUCCESS.to_u32(),
2279                3,
2280                11,
2281                7,
2282                create_response.encode(),
2283            ),
2284            response_frame(
2285                Command::SetInfo,
2286                NtStatus::SUCCESS.to_u32(),
2287                4,
2288                11,
2289                7,
2290                smolder_proto::smb::smb2::SetInfoResponse.encode(),
2291            ),
2292            response_frame(
2293                Command::Close,
2294                NtStatus::SUCCESS.to_u32(),
2295                5,
2296                11,
2297                7,
2298                CloseResponse {
2299                    flags: 0,
2300                    allocation_size: 5,
2301                    end_of_file: 5,
2302                    file_attributes: FileAttributes::ARCHIVE,
2303                }
2304                .encode(),
2305            ),
2306        ])
2307        .await;
2308
2309        share
2310            .rename("notes.txt", "archive\\notes.txt")
2311            .await
2312            .expect("rename should succeed");
2313    }
2314}