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), parse_with = binrw::helpers::until_eof)]
134    #[bw(if(get_quota_info_content.is_some()))]
135    #[bw(write_with = ChainedItem::write_chained_size_opt, args(&sid_list_length))]
136    pub get_quota_info_content: Option<Vec<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
148#[binrw::binrw]
149#[derive(Debug)]
150pub struct GetEaInfoList {
151    #[br(parse_with = binrw::helpers::until_eof)]
152    #[bw(write_with = FileGetEaInformation::write_chained)]
153    pub values: Vec<FileGetEaInformation>,
154}
155
156#[binrw::binrw]
157#[derive(Debug, PartialEq, Eq)]
158pub struct QueryInfoResponse {
159    #[bw(calc = 9)]
160    #[br(assert(_structure_size == 9))]
161    _structure_size: u16,
162    #[bw(calc = PosMarker::default())]
163    output_buffer_offset: PosMarker<u16>,
164    #[bw(calc = PosMarker::default())]
165    output_buffer_length: PosMarker<u32>,
166    #[br(seek_before = SeekFrom::Start(output_buffer_offset.value.into()))]
167    #[br(map_stream = |s| s.take_seek(output_buffer_length.value.into()))]
168    #[bw(write_with = PosMarker::write_aoff_size, args(&output_buffer_offset, &output_buffer_length))]
169    data: QueryInfoResponseData,
170}
171
172impl QueryInfoResponse {
173    /// Call this method first when parsing an incoming query info response.
174    /// It will parse the raw data into a [QueryInfoResponseData] struct, which has
175    /// a variation for each information type: File, FileSystem, Security, Quota.
176    /// This is done by calling the [QueryInfoResponseData::parse] method.
177    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
178        self.data.parse(info_type)
179    }
180}
181
182/// A helpers struct that contains the raw data of a query info response or a set info request,
183/// and can be parsed using the [`QueryInfoResponseData::parse`] method, to a specific info type.
184#[binrw::binrw]
185#[derive(Debug, PartialEq, Eq)]
186pub struct QueryInfoResponseData {
187    #[br(parse_with = binrw::helpers::until_eof)]
188    data: Vec<u8>,
189}
190
191impl QueryInfoResponseData {
192    pub fn parse(&self, info_type: InfoType) -> Result<QueryInfoData, binrw::Error> {
193        let mut cursor = Cursor::new(&self.data);
194        QueryInfoData::read_args(&mut cursor, (info_type,))
195    }
196}
197
198impl From<Vec<u8>> for QueryInfoResponseData {
199    fn from(data: Vec<u8>) -> Self {
200        QueryInfoResponseData { data }
201    }
202}
203
204query_info_data! {
205    QueryInfoData
206    File: RawQueryInfoData<QueryFileInfo>,
207    FileSystem: RawQueryInfoData<QueryFileSystemInfo>,
208    Security: SecurityDescriptor,
209    Quota: QueryQuotaInfo,
210}
211
212#[cfg(test)]
213mod tests {
214
215    use time::macros::datetime;
216
217    use crate::*;
218    use smb_dtyp::*;
219
220    use super::*;
221
222    #[test]
223    pub fn test_query_info_req_short_write() {
224        let data = encode_content(
225            QueryInfoRequest {
226                info_type: InfoType::File,
227                info_class: QueryInfoClass::File(QueryFileInfoClass::NetworkOpenInformation),
228                output_buffer_length: 56,
229                additional_info: AdditionalInfo::new(),
230                flags: QueryInfoFlags::new(),
231                file_id: [
232                    0x77, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xc5, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0,
233                    0x0,
234                ]
235                .into(),
236                data: GetInfoRequestData::None(()),
237            }
238            .into(),
239        );
240        assert_eq!(
241            data,
242            [
243                0x29, 0x0, 0x1, 0x22, 0x38, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
244                0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x77, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0,
245                0xc5, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0
246            ]
247        )
248    }
249
250    #[test]
251    pub fn test_query_info_ea_request() {
252        let req = QueryInfoRequest {
253            info_type: InfoType::File,
254            info_class: QueryInfoClass::File(QueryFileInfoClass::FullEaInformation),
255            additional_info: AdditionalInfo::new(),
256            flags: QueryInfoFlags::new()
257                .with_restart_scan(true)
258                .with_return_single_entry(true),
259            file_id: [
260                0x7a, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0xd1, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0,
261            ]
262            .into(),
263            data: GetInfoRequestData::EaInfo(GetEaInfoList {
264                values: vec![
265                    FileGetEaInformationInner {
266                        ea_name: "$MpEa_D262AC624451295".into(),
267                    }
268                    .into(),
269                ],
270            }),
271            output_buffer_length: 554,
272        };
273        let content_data = encode_content(req.into());
274        assert_eq!(
275            content_data,
276            [
277                0x29, 0x0, 0x1, 0xf, 0x2a, 0x2, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1b, 0x0, 0x0, 0x0,
278                0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x7a, 0x5, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0,
279                0xd1, 0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x24, 0x4d,
280                0x70, 0x45, 0x61, 0x5f, 0x44, 0x32, 0x36, 0x32, 0x41, 0x43, 0x36, 0x32, 0x34, 0x34,
281                0x35, 0x31, 0x32, 0x39, 0x35, 0x0
282            ]
283        )
284    }
285
286    #[test]
287    pub fn test_query_security_request() {
288        let res = encode_content(
289            QueryInfoRequest {
290                info_type: InfoType::Security,
291                info_class: Default::default(),
292                output_buffer_length: 0,
293                additional_info: AdditionalInfo::new()
294                    .with_owner_security_information(true)
295                    .with_group_security_information(true)
296                    .with_dacl_security_information(true)
297                    .with_sacl_security_information(true),
298                flags: QueryInfoFlags::new(),
299                file_id: guid!("0000002b-000d-0000-3100-00000d000000").into(),
300                data: GetInfoRequestData::None(()),
301            }
302            .into(),
303        );
304        assert_eq!(
305            res,
306            &[
307                0x29, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
308                0xf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2b, 0x0, 0x0, 0x0, 0xd, 0x0, 0x0, 0x0,
309                0x31, 0x0, 0x0, 0x0, 0xd, 0x0, 0x0, 0x0,
310            ]
311        );
312    }
313
314    #[test]
315    pub fn test_query_info_resp_parse_basic() {
316        let parsed = decode_content(&[
317            0xfe, 0x53, 0x4d, 0x42, 0x40, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x1, 0x0,
318            0x19, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
319            0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x69, 0x0, 0x0, 0x28, 0x0, 0x30, 0x0, 0x0, 0x77,
320            0x53, 0x6f, 0xb5, 0x9c, 0x21, 0xa4, 0xcd, 0x99, 0x9b, 0xc0, 0x87, 0xb9, 0x6, 0x83,
321            0xa3, 0x9, 0x0, 0x48, 0x0, 0x28, 0x0, 0x0, 0x0, 0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58,
322            0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51,
323            0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0,
324            0x0, 0x0, 0x0,
325        ]);
326        let parsed = parsed.content.to_queryinfo().unwrap();
327        assert_eq!(
328            parsed,
329            QueryInfoResponse {
330                data: [
331                    0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51,
332                    0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1,
333                    0xd, 0x51, 0x6b, 0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
334                ]
335                .to_vec()
336                .into(),
337            }
338        );
339    }
340
341    #[test]
342    pub fn test_query_info_resp_parse_file() {
343        let raw_data: QueryInfoResponseData = [
344            0x5b, 0x6c, 0x44, 0xce, 0x6a, 0x58, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb,
345            0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b, 0xdb, 0x1, 0x4, 0x8f, 0xa1, 0xd, 0x51, 0x6b,
346            0xdb, 0x1, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
347        ]
348        .to_vec()
349        .into();
350        assert_eq!(
351            raw_data
352                .parse(InfoType::File)
353                .unwrap()
354                .unwrap_file()
355                .parse(QueryFileInfoClass::BasicInformation)
356                .unwrap(),
357            QueryFileInfo::BasicInformation(FileBasicInformation {
358                creation_time: datetime!(2024-12-27 14:22:48.792994700).into(),
359                last_access_time: datetime!(2025-01-20 15:36:20.277632400).into(),
360                last_write_time: datetime!(2025-01-20 15:36:20.277632400).into(),
361                change_time: datetime!(2025-01-20 15:36:20.277632400).into(),
362                file_attributes: FileAttributes::new().with_archive(true)
363            })
364        )
365    }
366
367    #[test]
368    fn test_query_info_resp_parse_stream_info() {
369        let raw_data: QueryInfoResponseData = [
370            0x48, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00,
371            0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x5a, 0x00,
372            0x6f, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x2e, 0x00, 0x49, 0x00, 0x64, 0x00, 0x65, 0x00,
373            0x6e, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x65, 0x00, 0x72, 0x00,
374            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00, 0x00, 0x00,
375            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xd1, 0xd6, 0x00, 0x00,
376            0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00,
377            0x3a, 0x00, 0x24, 0x00, 0x44, 0x00, 0x41, 0x00, 0x54, 0x00, 0x41, 0x00,
378        ]
379        .to_vec()
380        .into();
381
382        assert_eq!(
383            raw_data
384                .parse(InfoType::File)
385                .unwrap()
386                .unwrap_file()
387                .parse(QueryFileInfoClass::StreamInformation)
388                .unwrap(),
389            QueryFileInfo::StreamInformation(
390                vec![
391                    FileStreamInformationInner {
392                        stream_size: 0x93,
393                        stream_allocation_size: 0x1000,
394                        stream_name: SizedWideString::from(":Zone.Identifier:$DATA"),
395                    },
396                    FileStreamInformationInner {
397                        stream_size: 0xd6d1,
398                        stream_allocation_size: 0xd000,
399                        stream_name: SizedWideString::from("::$DATA"),
400                    },
401                ]
402                .into()
403            )
404        )
405    }
406}