smb_msg/info/
query.rs

1//! Get/Set Info Request/Response
2
3use crate::FileId;
4use binrw::{io::TakeSeekExt, prelude::*};
5use modular_bitfield::prelude::*;
6use smb_dtyp::{SID, SecurityDescriptor, binrw_util::prelude::*};
7use smb_msg_derive::*;
8use std::io::{Cursor, SeekFrom};
9
10use super::common::*;
11use smb_fscc::*;
12
13/// Request to query information on a file, named pipe, or underlying volume.
14///
15/// MS-SMB2 2.2.37
16#[smb_request(size = 41)]
17pub struct QueryInfoRequest {
18    /// The type of information queried (file, filesystem, security, or quota).
19    pub info_type: InfoType,
20    /// For file/filesystem queries, specifies the information class to retrieve.
21    #[brw(args(info_type))]
22    pub info_class: QueryInfoClass,
23
24    /// Maximum number of bytes the server can send in the response.
25    pub output_buffer_length: u32,
26    #[bw(calc = PosMarker::default())]
27    #[br(temp)]
28    _input_buffer_offset: PosMarker<u16>,
29    reserved: u16,
30    #[bw(calc = PosMarker::default())]
31    #[br(temp)]
32    input_buffer_length: PosMarker<u32>,
33    /// Provides additional information for security or EA queries.
34    /// For security queries, contains flags indicating which security attributes to return.
35    /// For EA queries without an EA list, contains index to start enumeration.
36    pub additional_info: AdditionalInfo,
37    /// Flags for EA enumeration control (restart scan, return single entry, index specified).
38    pub flags: QueryInfoFlags,
39    /// Identifier of the file or named pipe on which to perform the query.
40    pub file_id: FileId,
41    /// Input data for quota or EA queries. Empty for other information types.
42    #[br(map_stream = |s| s.take_seek(input_buffer_length.value as u64))]
43    #[br(args(&info_class, info_type))]
44    #[bw(write_with = PosMarker::write_aoff_size_a, args(&_input_buffer_offset, &input_buffer_length, (info_class, *info_type)))]
45    pub data: GetInfoRequestData,
46}
47
48/// Helper enum to specify the information class for query info requests,
49/// when it is applicable.
50#[smb_request_binrw]
51#[br(import(info_type: InfoType))]
52#[bw(import(info_type: &InfoType))]
53pub enum QueryInfoClass {
54    #[br(pre_assert(matches!(info_type, InfoType::File)))]
55    #[bw(assert(matches!(info_type, InfoType::File)))]
56    File(QueryFileInfoClass),
57
58    #[br(pre_assert(matches!(info_type, InfoType::FileSystem)))]
59    #[bw(assert(matches!(info_type, InfoType::FileSystem)))]
60    FileSystem(QueryFileSystemInfoClass),
61
62    Empty(NullByte),
63}
64
65impl Default for QueryInfoClass {
66    fn default() -> Self {
67        QueryInfoClass::Empty(NullByte {})
68    }
69}
70
71/// Single null (0) byte.
72///
73/// - When reading, asserts that the byte is 0.
74/// - When writing, always writes a 0 byte.
75#[binrw::binrw]
76#[derive(Debug, PartialEq, Eq, Default)]
77pub struct NullByte {
78    #[bw(calc = 0)]
79    #[br(assert(_null == 0))]
80    _null: u8,
81}
82
83impl AdditionalInfo {
84    pub fn is_security(&self) -> bool {
85        self.owner_security_information()
86            || self.group_security_information()
87            || self.dacl_security_information()
88            || self.sacl_security_information()
89            || self.label_security_information()
90            || self.attribute_security_information()
91            || self.scope_security_information()
92            || self.backup_security_information()
93    }
94}
95
96#[smb_dtyp::mbitfield]
97pub struct QueryInfoFlags {
98    /// Restart the scan for EAs from the beginning.
99    pub restart_scan: bool,
100    /// Return a single EA entry in the response buffer.
101    pub return_single_entry: bool,
102    /// The caller has specified an EA index.
103    pub index_specified: bool,
104    #[skip]
105    __: B29,
106}
107
108/// Input data for query information requests that require additional parameters.
109///
110/// This payload is used for quota and extended attribute queries.
111/// Other information types have no input data.
112///
113/// MS-SMB2 2.2.37
114#[smb_request_binrw]
115#[brw(import(file_info_class: &QueryInfoClass, query_info_type: InfoType))]
116pub enum GetInfoRequestData {
117    /// The query quota to perform.
118    #[br(pre_assert(query_info_type == InfoType::Quota))]
119    #[bw(assert(query_info_type == InfoType::Quota))]
120    Quota(QueryQuotaInfo),
121
122    /// Extended attributes information to query.
123    #[br(pre_assert(matches!(file_info_class, QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)) && query_info_type == InfoType::File))]
124    #[bw(assert(matches!(file_info_class, QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)) && query_info_type == InfoType::File))]
125    EaInfo(GetEaInfoList),
126
127    // Other cases have no data.
128    #[br(pre_assert(query_info_type != InfoType::Quota && !(query_info_type == InfoType::File && matches!(file_info_class , QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)))))]
129    None(()),
130}
131
132/// Specifies the quota information to query.
133///
134/// MS-SMB2 2.2.37.1
135#[smb_message_binrw]
136pub struct QueryQuotaInfo {
137    /// If true, server returns a single quota entry. Otherwise, returns maximum entries that fit.
138    pub return_single: Boolean,
139    /// If true, quota information is read from the beginning. Otherwise, continues from previous enumeration.
140    pub restart_scan: Boolean,
141    reserved: u16,
142    #[bw(calc = PosMarker::default())]
143    #[br(temp)]
144    sid_list_length: PosMarker<u32>, // type 1: list of FileGetQuotaInformation structs.
145    #[bw(calc = PosMarker::default())]
146    #[br(temp)]
147    start_sid_length: PosMarker<u32>, // type 2: SIDs list
148    #[bw(calc = PosMarker::default())]
149    #[br(temp)]
150    start_sid_offset: PosMarker<u32>,
151
152    /// Option 1: List of FileGetQuotaInformation structs to query specific quota entries.
153    #[br(if(sid_list_length.value > 0))]
154    #[br(map_stream = |s| s.take_seek(sid_list_length.value as u64))]
155    #[bw(if(get_quota_info_content.as_ref().is_some_and(|v| !v.is_empty())))]
156    #[bw(write_with = PosMarker::write_size, args(&sid_list_length))]
157    pub get_quota_info_content: Option<ChainedItemList<FileGetQuotaInformation>>,
158
159    /// Option 2: Single SID to query quota for a specific user.
160    #[br(if(start_sid_length.value > 0))]
161    #[bw(if(sid.is_some()))]
162    #[br(seek_before = SeekFrom::Current(start_sid_offset.value as i64))]
163    #[bw(write_with = PosMarker::write_size, args(&start_sid_length))]
164    #[brw(assert(get_quota_info_content.is_none() != sid.is_none()))]
165    // offset is 0, the default anyway.
166    pub sid: Option<SID>,
167}
168
169impl QueryQuotaInfo {
170    /// Builds a new [`QueryQuotaInfo`] with a list of [`FileGetQuotaInformation`] structs.
171    ///
172    /// MS-SMB2 2.2.37.1 Option 1
173    pub fn new(
174        return_single: bool,
175        restart_scan: bool,
176        content: Vec<FileGetQuotaInformation>,
177    ) -> Self {
178        Self {
179            return_single: return_single.into(),
180            restart_scan: restart_scan.into(),
181            get_quota_info_content: Some(content.into()),
182            sid: None,
183        }
184    }
185
186    /// Builds a new [`QueryQuotaInfo`] with a single SID.
187    ///
188    /// MS-SMB2 2.2.37.1 Option 2
189    pub fn new_sid(return_single: bool, restart_scan: bool, sid: SID) -> Self {
190        Self {
191            return_single: return_single.into(),
192            restart_scan: restart_scan.into(),
193            get_quota_info_content: None,
194            sid: Some(sid),
195        }
196    }
197}
198
199#[derive(BinRead, BinWrite, Debug, PartialEq, Eq)]
200pub struct GetEaInfoList {
201    pub values: ChainedItemList<FileGetEaInformation>,
202}
203
204/// Response to a query information request, containing the requested data.
205///
206/// MS-SMB2 2.2.38
207#[smb_response(size = 9)]
208pub struct QueryInfoResponse {
209    #[bw(calc = PosMarker::default())]
210    #[br(temp)]
211    output_buffer_offset: PosMarker<u16>,
212    #[bw(calc = PosMarker::default())]
213    #[br(temp)]
214    output_buffer_length: PosMarker<u32>,
215    /// The information being returned. Format depends on the info type and additional information from the request.
216    #[br(seek_before = SeekFrom::Start(output_buffer_offset.value.into()))]
217    #[br(map_stream = |s| s.take_seek(output_buffer_length.value.into()))]
218    #[bw(write_with = PosMarker::write_aoff_size, args(&output_buffer_offset, &output_buffer_length))]
219    data: QueryInfoResponseData,
220}
221
222impl QueryInfoResponse {
223    /// Call this method first when parsing an incoming query info response.
224    /// It will parse the raw data into a [QueryInfoResponseData] struct, which has
225    /// a variation for each information type: File, FileSystem, Security, Quota.
226    /// This is done by calling the [QueryInfoResponseData::parse] method.
227    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
228        self.data.parse(info_type)
229    }
230}
231
232/// A helper structure containing raw response data that can be parsed into specific information types.
233///
234/// Call [`QueryInfoResponseData::parse`] to convert to the appropriate data format
235/// based on the info type from the request.
236#[smb_response_binrw]
237pub struct QueryInfoResponseData {
238    #[br(parse_with = binrw::helpers::until_eof)]
239    data: Vec<u8>,
240}
241
242impl QueryInfoResponseData {
243    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
244        let mut cursor = Cursor::new(&self.data);
245        QueryInfoData::read_args(&mut cursor, (info_type,))
246    }
247}
248
249impl From<Vec<u8>> for QueryInfoResponseData {
250    fn from(data: Vec<u8>) -> Self {
251        QueryInfoResponseData { data }
252    }
253}
254
255query_info_data! {
256    QueryInfoData
257    File: RawQueryInfoData<QueryFileInfo>,
258    FileSystem: RawQueryInfoData<QueryFileSystemInfo>,
259    Security: SecurityDescriptor,
260    Quota: ChainedItemList<FileQuotaInformation>,
261}
262
263#[cfg(test)]
264mod tests {
265
266    use time::macros::datetime;
267
268    use crate::*;
269    use smb_dtyp::*;
270
271    use super::*;
272
273    const QUERY_INFO_HEADER_DATA: &'static str = "";
274
275    test_request! {
276        query_info_basic: QueryInfo {
277            info_type: InfoType::File,
278            info_class: QueryInfoClass::File(QueryFileInfoClass::NetworkOpenInformation),
279            output_buffer_length: 56,
280            additional_info: AdditionalInfo::new(),
281            flags: QueryInfoFlags::new(),
282            file_id: [
283                0x77, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xc5, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0,
284                0x0,
285            ]
286            .into(),
287            data: GetInfoRequestData::None(()),
288        } => const_format::concatcp!(QUERY_INFO_HEADER_DATA, "290001223800000068000000000000000000000000000000770500000c000000c50010000c000000")
289    }
290
291    test_request! {
292        query_info_get_ea: QueryInfo {
293            info_type: InfoType::File,
294            info_class: QueryInfoClass::File(QueryFileInfoClass::FullEaInformation),
295            additional_info: AdditionalInfo::new(),
296            flags: QueryInfoFlags::new()
297                .with_restart_scan(true)
298                .with_return_single_entry(true),
299            file_id: [
300                0x7a, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xd1, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0,
301            ]
302            .into(),
303            data: GetInfoRequestData::EaInfo(GetEaInfoList {
304                values: vec![FileGetEaInformation::new("$MpEa_D262AC624451295")].into(),
305            }),
306            output_buffer_length: 554,
307        } => const_format::concatcp!(QUERY_INFO_HEADER_DATA, "2900010f2a020000680000001b00000000000000030000007a0500000c000000d10010000c0000000000000015244d7045615f44323632414336323434353132393500")
308    }
309
310    test_request! {
311        query_security: QueryInfo {
312            info_type: InfoType::Security,
313            info_class: Default::default(),
314            output_buffer_length: 0,
315            additional_info: AdditionalInfo::new()
316                .with_owner_security_information(true)
317                .with_group_security_information(true)
318                .with_dacl_security_information(true)
319                .with_sacl_security_information(true),
320            flags: QueryInfoFlags::new(),
321            file_id: make_guid!("0000002b-000d-0000-3100-00000d000000").into(),
322            data: GetInfoRequestData::None(()),
323        } => const_format::concatcp!(QUERY_INFO_HEADER_DATA, "290003000000000068000000000000000f000000000000002b0000000d000000310000000d000000")
324    }
325
326    test_response! {
327        QueryInfo {
328            data: [
329                0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51,
330                0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1,
331                0xd, 0x51, 0x6b, 0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
332            ]
333            .to_vec()
334            .into()
335        } => "09004800280000005b6c44ce6a58db01048fa10d516bdb01048fa10d516bdb01048fa10d516bdb012000000000000000"
336    }
337
338    #[test]
339    pub fn test_query_info_resp_parse_file() {
340        let raw_data: QueryInfoResponseData = [
341            0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb,
342            0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b,
343            0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
344        ]
345        .to_vec()
346        .into();
347        assert_eq!(
348            raw_data
349                .parse(InfoType::File)
350                .unwrap()
351                .as_file()
352                .unwrap()
353                .parse(QueryFileInfoClass::BasicInformation)
354                .unwrap(),
355            QueryFileInfo::BasicInformation(FileBasicInformation {
356                creation_time: datetime!(2024-12-27 14:22:48.792994700).into(),
357                last_access_time: datetime!(2025-01-20 15:36:20.277632400).into(),
358                last_write_time: datetime!(2025-01-20 15:36:20.277632400).into(),
359                change_time: datetime!(2025-01-20 15:36:20.277632400).into(),
360                file_attributes: FileAttributes::new().with_archive(true)
361            })
362        )
363    }
364
365    #[test]
366    fn test_query_info_resp_parse_stream_info() {
367        let raw_data: QueryInfoResponseData = [
368            0x48, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00,
369            0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x5a, 0x00,
370            0x6f, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x2e, 0x00, 0x49, 0x00, 0x64, 0x00, 0x65, 0x00,
371            0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x65, 0x00, 0x72, 0x00,
372            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00, 0x00, 0x00,
373            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd1, 0xd6, 0x00, 0x00,
374            0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00,
375            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00,
376        ]
377        .to_vec()
378        .into();
379
380        assert_eq!(
381            raw_data
382                .parse(InfoType::File)
383                .unwrap()
384                .as_file()
385                .unwrap()
386                .parse(QueryFileInfoClass::StreamInformation)
387                .unwrap(),
388            QueryFileInfo::StreamInformation(
389                vec![
390                    FileStreamInformationInner {
391                        stream_size: 0x93,
392                        stream_allocation_size: 0x1000,
393                        stream_name: SizedWideString::from(":Zone.Identifier:$DATA"),
394                    },
395                    FileStreamInformationInner {
396                        stream_size: 0xd6d1,
397                        stream_allocation_size: 0xd000,
398                        stream_name: SizedWideString::from("::$DATA"),
399                    },
400                ]
401                .into()
402            )
403        )
404    }
405}