smb_msg/info/
query.rs

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