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)]
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    #[br(assert(_reserved == 0))]
27    #[bw(calc = 0)]
28    _reserved: u16,
29    #[bw(calc = PosMarker::default())]
30    input_buffer_length: PosMarker<u32>,
31    pub additional_info: AdditionalInfo,
32    pub flags: QueryInfoFlags,
33    pub file_id: FileId,
34    #[br(map_stream = |s| s.take_seek(input_buffer_length.value as u64))]
35    #[br(args(&info_class, info_type))]
36    #[bw(write_with = PosMarker::write_aoff_size_a, args(&_input_buffer_offset, &input_buffer_length, (info_class, *info_type)))]
37    pub data: GetInfoRequestData,
38}
39
40#[binrw::binrw]
41#[derive(Debug, PartialEq, Eq)]
42#[br(import(info_type: InfoType))]
43#[bw(import(info_type: &InfoType))]
44pub enum QueryInfoClass {
45    #[br(pre_assert(matches!(info_type, InfoType::File)))]
46    #[bw(assert(matches!(info_type, InfoType::File)))]
47    File(QueryFileInfoClass),
48
49    #[br(pre_assert(matches!(info_type, InfoType::FileSystem)))]
50    #[bw(assert(matches!(info_type, InfoType::FileSystem)))]
51    FileSystem(QueryFileSystemInfoClass),
52
53    Empty(NullByte),
54}
55
56impl Default for QueryInfoClass {
57    fn default() -> Self {
58        QueryInfoClass::Empty(NullByte {})
59    }
60}
61
62#[binrw::binrw]
63#[derive(Debug, PartialEq, Eq, Default)]
64pub struct NullByte {
65    #[bw(calc = 0)]
66    #[br(assert(_null == 0))]
67    _null: u8,
68}
69
70impl AdditionalInfo {
71    pub fn is_security(&self) -> bool {
72        self.owner_security_information()
73            || self.group_security_information()
74            || self.dacl_security_information()
75            || self.sacl_security_information()
76            || self.label_security_information()
77            || self.attribute_security_information()
78            || self.scope_security_information()
79            || self.backup_security_information()
80    }
81}
82
83#[bitfield]
84#[derive(BinWrite, BinRead, Debug, Default, Clone, Copy, PartialEq, Eq)]
85#[bw(map = |&x| Self::into_bytes(x))]
86#[br(map = Self::from_bytes)]
87pub struct QueryInfoFlags {
88    pub restart_scan: bool,
89    pub return_single_entry: bool,
90    pub index_specified: bool,
91    #[skip]
92    __: B29,
93}
94
95/// This struct describes the payload to be added in the [QueryInfoRequest]
96/// when asking for information about Quota or Extended Attributes.
97/// In other cases, it is empty.
98#[binrw::binrw]
99#[derive(Debug)]
100#[brw(import(file_info_class: &QueryInfoClass, query_info_type: InfoType))]
101pub enum GetInfoRequestData {
102    /// The query quota to perform.
103    #[br(pre_assert(query_info_type == InfoType::Quota))]
104    #[bw(assert(query_info_type == InfoType::Quota))]
105    Quota(QueryQuotaInfo),
106
107    /// Extended attributes information to query.
108    #[br(pre_assert(matches!(file_info_class, QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)) && query_info_type == InfoType::File))]
109    #[bw(assert(matches!(file_info_class, QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)) && query_info_type == InfoType::File))]
110    EaInfo(GetEaInfoList),
111
112    // Other cases have no data.
113    #[br(pre_assert(query_info_type != InfoType::Quota && !(query_info_type == InfoType::File && matches!(file_info_class , QueryInfoClass::File(QueryFileInfoClass::FullEaInformation)))))]
114    None(()),
115}
116
117#[binrw::binrw]
118#[derive(Debug)]
119pub struct QueryQuotaInfo {
120    pub return_single: Boolean,
121    pub restart_scan: Boolean,
122    #[bw(calc = 0)]
123    _reserved: u16,
124    #[bw(calc = PosMarker::default())]
125    sid_list_length: PosMarker<u32>, // type 1: list of FileGetQuotaInformation structs.
126    #[bw(calc = PosMarker::default())]
127    start_sid_length: PosMarker<u32>, // type 2: SIDs list
128    #[bw(calc = PosMarker::default())]
129    start_sid_offset: PosMarker<u32>,
130
131    /// Option 1: list of FileGetQuotaInformation structs.
132    #[br(if(sid_list_length.value > 0))]
133    #[br(map_stream = |s| s.take_seek(sid_list_length.value as u64))]
134    #[bw(if(get_quota_info_content.as_ref().is_some_and(|v| !v.is_empty())))]
135    #[bw(write_with = PosMarker::write_size, args(&sid_list_length))]
136    pub get_quota_info_content: Option<ChainedItemList<FileGetQuotaInformation>>,
137
138    /// Option 2: SID (usually not used).
139    #[br(if(start_sid_length.value > 0))]
140    #[bw(if(sid.is_some()))]
141    #[br(seek_before = SeekFrom::Current(start_sid_offset.value as i64))]
142    #[bw(write_with = PosMarker::write_size, args(&start_sid_length))]
143    #[brw(assert(get_quota_info_content.is_none() != sid.is_none()))]
144    // offset is 0, the default anyway.
145    pub sid: Option<SID>,
146}
147
148impl QueryQuotaInfo {
149    /// Builds a new [`QueryQuotaInfo`] with a list of [`FileGetQuotaInformation`] structs.
150    ///
151    /// Option #1 under 2.2.37.1
152    pub fn new(
153        return_single: bool,
154        restart_scan: bool,
155        content: Vec<FileGetQuotaInformation>,
156    ) -> Self {
157        Self {
158            return_single: return_single.into(),
159            restart_scan: restart_scan.into(),
160            get_quota_info_content: Some(content.into()),
161            sid: None,
162        }
163    }
164
165    /// Builds a new [`QueryQuotaInfo`] with a single SID.
166    ///
167    /// Option #2 under 2.2.37.1
168    pub fn new_sid(return_single: bool, restart_scan: bool, sid: SID) -> Self {
169        Self {
170            return_single: return_single.into(),
171            restart_scan: restart_scan.into(),
172            get_quota_info_content: None,
173            sid: Some(sid),
174        }
175    }
176}
177
178#[derive(BinRead, BinWrite, Debug)]
179pub struct GetEaInfoList {
180    pub values: ChainedItemList<FileGetEaInformation>,
181}
182
183#[binrw::binrw]
184#[derive(Debug, PartialEq, Eq)]
185pub struct QueryInfoResponse {
186    #[bw(calc = 9)]
187    #[br(assert(_structure_size == 9))]
188    _structure_size: u16,
189    #[bw(calc = PosMarker::default())]
190    output_buffer_offset: PosMarker<u16>,
191    #[bw(calc = PosMarker::default())]
192    output_buffer_length: PosMarker<u32>,
193    #[br(seek_before = SeekFrom::Start(output_buffer_offset.value.into()))]
194    #[br(map_stream = |s| s.take_seek(output_buffer_length.value.into()))]
195    #[bw(write_with = PosMarker::write_aoff_size, args(&output_buffer_offset, &output_buffer_length))]
196    data: QueryInfoResponseData,
197}
198
199impl QueryInfoResponse {
200    /// Call this method first when parsing an incoming query info response.
201    /// It will parse the raw data into a [QueryInfoResponseData] struct, which has
202    /// a variation for each information type: File, FileSystem, Security, Quota.
203    /// This is done by calling the [QueryInfoResponseData::parse] method.
204    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
205        self.data.parse(info_type)
206    }
207}
208
209/// A helpers struct that contains the raw data of a query info response or a set info request,
210/// and can be parsed using the [`QueryInfoResponseData::parse`] method, to a specific info type.
211#[binrw::binrw]
212#[derive(Debug, PartialEq, Eq)]
213pub struct QueryInfoResponseData {
214    #[br(parse_with = binrw::helpers::until_eof)]
215    data: Vec<u8>,
216}
217
218impl QueryInfoResponseData {
219    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
220        let mut cursor = Cursor::new(&self.data);
221        QueryInfoData::read_args(&mut cursor, (info_type,))
222    }
223}
224
225impl From<Vec<u8>> for QueryInfoResponseData {
226    fn from(data: Vec<u8>) -> Self {
227        QueryInfoResponseData { data }
228    }
229}
230
231query_info_data! {
232    QueryInfoData
233    File: RawQueryInfoData<QueryFileInfo>,
234    FileSystem: RawQueryInfoData<QueryFileSystemInfo>,
235    Security: SecurityDescriptor,
236    Quota: ChainedItemList<FileQuotaInformation>,
237}
238
239#[cfg(test)]
240mod tests {
241
242    use time::macros::datetime;
243
244    use crate::*;
245    use smb_dtyp::*;
246
247    use super::*;
248
249    #[test]
250    pub fn test_query_info_req_short_write() {
251        let data = encode_content(
252            QueryInfoRequest {
253                info_type: InfoType::File,
254                info_class: QueryInfoClass::File(QueryFileInfoClass::NetworkOpenInformation),
255                output_buffer_length: 56,
256                additional_info: AdditionalInfo::new(),
257                flags: QueryInfoFlags::new(),
258                file_id: [
259                    0x77, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xc5, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0,
260                    0x0,
261                ]
262                .into(),
263                data: GetInfoRequestData::None(()),
264            }
265            .into(),
266        );
267        assert_eq!(
268            data,
269            [
270                0x29, 0x0, 0x1, 0x22, 0x38, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
271                0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x77, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0,
272                0xc5, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0
273            ]
274        )
275    }
276
277    #[test]
278    pub fn test_query_info_ea_request() {
279        let req = QueryInfoRequest {
280            info_type: InfoType::File,
281            info_class: QueryInfoClass::File(QueryFileInfoClass::FullEaInformation),
282            additional_info: AdditionalInfo::new(),
283            flags: QueryInfoFlags::new()
284                .with_restart_scan(true)
285                .with_return_single_entry(true),
286            file_id: [
287                0x7a, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xd1, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0,
288            ]
289            .into(),
290            data: GetInfoRequestData::EaInfo(GetEaInfoList {
291                values: vec![FileGetEaInformation::new("$MpEa_D262AC624451295")].into(),
292            }),
293            output_buffer_length: 554,
294        };
295        let content_data = encode_content(req.into());
296        assert_eq!(
297            content_data,
298            [
299                0x29, 0x0, 0x1, 0xf, 0x2a, 0x2, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1b, 0x0, 0x0, 0x0,
300                0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x7a, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0,
301                0xd1, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x24, 0x4d,
302                0x70, 0x45, 0x61, 0x5f, 0x44, 0x32, 0x36, 0x32, 0x41, 0x43, 0x36, 0x32, 0x34, 0x34,
303                0x35, 0x31, 0x32, 0x39, 0x35, 0x0
304            ]
305        )
306    }
307
308    #[test]
309    pub fn test_query_security_request() {
310        let res = encode_content(
311            QueryInfoRequest {
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            }
324            .into(),
325        );
326        assert_eq!(
327            res,
328            &[
329                0x29, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
330                0xf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2b, 0x0, 0x0, 0x0, 0xd, 0x0, 0x0, 0x0,
331                0x31, 0x0, 0x0, 0x0, 0xd, 0x0, 0x0, 0x0,
332            ]
333        );
334    }
335
336    #[test]
337    pub fn test_query_info_resp_parse_basic() {
338        let parsed = decode_content(&[
339            0xfe, 0x53, 0x4d, 0x42, 0x40, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x1, 0x0,
340            0x19, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
341            0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x69, 0x0, 0x0, 0x28, 0x0, 0x30, 0x0, 0x0, 0x77,
342            0x53, 0x6f, 0xb5, 0x9c, 0x21, 0xa4, 0xcd, 0x99, 0x9b, 0xc0, 0x87, 0xb9, 0x6, 0x83,
343            0xa3, 0x9, 0x0, 0x48, 0x0, 0x28, 0x0, 0x0, 0x0, 0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58,
344            0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51,
345            0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0,
346            0x0, 0x0, 0x0,
347        ]);
348        let parsed = parsed.content.to_queryinfo().unwrap();
349        assert_eq!(
350            parsed,
351            QueryInfoResponse {
352                data: [
353                    0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51,
354                    0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1,
355                    0xd, 0x51, 0x6b, 0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
356                ]
357                .to_vec()
358                .into(),
359            }
360        );
361    }
362
363    #[test]
364    pub fn test_query_info_resp_parse_file() {
365        let raw_data: QueryInfoResponseData = [
366            0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb,
367            0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b,
368            0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
369        ]
370        .to_vec()
371        .into();
372        assert_eq!(
373            raw_data
374                .parse(InfoType::File)
375                .unwrap()
376                .as_file()
377                .unwrap()
378                .parse(QueryFileInfoClass::BasicInformation)
379                .unwrap(),
380            QueryFileInfo::BasicInformation(FileBasicInformation {
381                creation_time: datetime!(2024-12-27 14:22:48.792994700).into(),
382                last_access_time: datetime!(2025-01-20 15:36:20.277632400).into(),
383                last_write_time: datetime!(2025-01-20 15:36:20.277632400).into(),
384                change_time: datetime!(2025-01-20 15:36:20.277632400).into(),
385                file_attributes: FileAttributes::new().with_archive(true)
386            })
387        )
388    }
389
390    #[test]
391    fn test_query_info_resp_parse_stream_info() {
392        let raw_data: QueryInfoResponseData = [
393            0x48, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00,
394            0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x5a, 0x00,
395            0x6f, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x2e, 0x00, 0x49, 0x00, 0x64, 0x00, 0x65, 0x00,
396            0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x65, 0x00, 0x72, 0x00,
397            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00, 0x00, 0x00,
398            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd1, 0xd6, 0x00, 0x00,
399            0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00,
400            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00,
401        ]
402        .to_vec()
403        .into();
404
405        assert_eq!(
406            raw_data
407                .parse(InfoType::File)
408                .unwrap()
409                .as_file()
410                .unwrap()
411                .parse(QueryFileInfoClass::StreamInformation)
412                .unwrap(),
413            QueryFileInfo::StreamInformation(
414                vec![
415                    FileStreamInformationInner {
416                        stream_size: 0x93,
417                        stream_allocation_size: 0x1000,
418                        stream_name: SizedWideString::from(":Zone.Identifier:$DATA"),
419                    },
420                    FileStreamInformationInner {
421                        stream_size: 0xd6d1,
422                        stream_allocation_size: 0xd000,
423                        stream_name: SizedWideString::from("::$DATA"),
424                    },
425                ]
426                .into()
427            )
428        )
429    }
430}