Skip to main content

everything_ipc/wm/
types.rs

1use std::mem;
2
3use derive_more::TryFrom;
4use widestring::{U16CStr, u16cstr};
5use windows::{Win32::Foundation::FILETIME, core::PCWSTR};
6
7pub const EVERYTHING_IPC_GET_MINOR_VERSION: u32 = 1;
8
9pub const EVERYTHING_IPC_COPYDATA_QUERY2W: u32 = 18;
10
11pub const EVERYTHING_IPC_IS_DB_LOADED: u32 = 401;
12pub const EVERYTHING_IPC_IS_FILE_INFO_INDEXED: u32 = 411;
13
14/// Register window message for IPC creation
15pub fn register_ipc_created_message() -> u32 {
16    use windows::Win32::UI::WindowsAndMessaging::RegisterWindowMessageW;
17
18    unsafe { RegisterWindowMessageW(PCWSTR(u16cstr!("EVERYTHING_IPC_CREATED").as_ptr())) }
19}
20
21/// Search flags for Everything IPC
22#[repr(transparent)]
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub struct SearchFlags(u32);
25
26bitflags::bitflags! {
27    impl SearchFlags: u32 {
28        const MatchCase = 0x00000001;
29        const MatchWholeWord = 0x00000002;
30        const MatchPath = 0x00000004;
31        const Regex = 0x00000008;
32        /// Abandoned?
33        const MatchAccents = 0x00000010;
34    }
35}
36
37/// Sort types for Everything IPC
38#[repr(u32)]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, TryFrom)]
40#[try_from(repr)]
41pub enum Sort {
42    /// Best performance
43    #[default]
44    NameAscending = 1,
45    NameDescending = 2,
46    PathAscending = 3,
47    PathDescending = 4,
48    SizeAscending = 5,
49    SizeDescending = 6,
50    ExtensionAscending = 7,
51    ExtensionDescending = 8,
52    TypeNameAscending = 9,
53    TypeNameDescending = 10,
54    DateCreatedAscending = 11,
55    DateCreatedDescending = 12,
56    DateModifiedAscending = 13,
57    DateModifiedDescending = 14,
58    AttributesAscending = 15,
59    AttributesDescending = 16,
60    FileListFilenameAscending = 17,
61    FileListFilenameDescending = 18,
62    RunCountAscending = 19,
63    RunCountDescending = 20,
64    DateRecentlyChangedAscending = 21,
65    DateRecentlyChangedDescending = 22,
66    DateAccessedAscending = 23,
67    DateAccessedDescending = 24,
68    DateRunAscending = 25,
69    DateRunDescending = 26,
70}
71
72/// File info types for Everything
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum FileInfo {
75    FileSize = 1,
76    FolderSize = 2,
77    DateCreated = 3,
78    DateModified = 4,
79    DateAccessed = 5,
80    Attributes = 6,
81}
82
83/// Request flags for Everything IPC
84#[repr(transparent)]
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub struct RequestFlags(u32);
87
88bitflags::bitflags! {
89    impl RequestFlags: u32 {
90        const FileName = 0x00000001;
91        const Path = 0x00000002;
92        const FullPathAndFileName = 0x00000004;
93        const Extension = 0x00000008;
94        const Size = 0x00000010;
95        const DateCreated = 0x00000020;
96        const DateModified = 0x00000040;
97        const DateAccessed = 0x00000080;
98        const Attributes = 0x00000100;
99        const FileListFileName = 0x00000200;
100        const RunCount = 0x00000400;
101        const DateRun = 0x00000800;
102        const DateRecentlyChanged = 0x00001000;
103        const HighlightedFileName = 0x00002000;
104        const HighlightedPath = 0x00004000;
105        const HighlightedFullPathAndFileName = 0x00008000;
106    }
107}
108
109/// Request data type
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum RequestDataType {
112    Str,
113    Size,
114    Date,
115    Dword,
116}
117
118impl RequestFlags {
119    /// Returns the data type for a given request flag
120    pub fn data_type(self) -> RequestDataType {
121        match self {
122            Self::FileName
123            | Self::Path
124            | Self::FullPathAndFileName
125            | Self::Extension
126            | Self::FileListFileName
127            | Self::HighlightedFileName
128            | Self::HighlightedPath
129            | Self::HighlightedFullPathAndFileName => RequestDataType::Str,
130            Self::Size => RequestDataType::Size,
131            Self::DateCreated
132            | Self::DateModified
133            | Self::DateAccessed
134            | Self::DateRun
135            | Self::DateRecentlyChanged => RequestDataType::Date,
136            Self::Attributes | Self::RunCount => RequestDataType::Dword,
137            _ => panic!("Invalid request flag"),
138        }
139    }
140}
141
142/// Query item value - the result of a query request
143#[derive(Debug, Clone, Copy, PartialEq)]
144pub enum QueryValue<'a> {
145    /// String value (file name, path, extension, etc.)
146    Str(&'a U16CStr),
147    /// Size value (file size in bytes)
148    Size(u64),
149    /// Date value (FILETIME)
150    Time(FILETIME),
151    /// Dword value (attributes, run count)
152    U32(u32),
153}
154
155/// Query results header from Everything
156/// Matches `EVERYTHING_IPC_LIST2` from `Everything.h`
157#[repr(C)]
158#[derive(Debug, Clone, Copy)]
159pub struct EverythingIpcList2 {
160    pub totitems: u32,      // found items
161    pub numitems: u32,      // available items
162    pub offset: u32,        // offset of the first result
163    pub request_flags: u32, // valid request flags
164    pub sort_type: u32,     // actual sort type
165}
166
167/// A single item in query results
168/// Matches `EVERYTHING_IPC_ITEM2` from `Everything.h`
169#[repr(C)]
170#[derive(Debug, Clone, Copy)]
171pub struct EverythingIpcItem2 {
172    /// TODO
173    pub flags: u32,
174    pub data_offset: u32,
175}
176
177/// Query request data for Everything IPC
178/// Matches `EVERYTHING_IPC_QUERY2` from `Everything.h`
179/// Note: The SDK uses a flexible array member, so we use a pointer-based approach
180/// to avoid padding issues.
181#[repr(C, packed)]
182pub struct EverythingIpcQuery2 {
183    pub reply_hwnd: u32,
184    pub reply_copydata_message: u32,
185    pub search_flags: u32,
186    pub offset: u32,
187    pub max_results: u32,
188    pub request_flags: u32,
189    pub sort_type: u32,
190    /// Flexible array member, at least 1
191    pub search_string: [u16; 1],
192}
193
194impl EverythingIpcQuery2 {
195    /// Calculate the total size needed for a query with given search string length
196    /// Header is exactly 28 bytes (7 DWORDs) + minimum 2 bytes for search_string
197    pub fn size_for_search(search_len: usize) -> usize {
198        mem::size_of::<Self>() + (search_len * mem::size_of::<u16>())
199    }
200
201    /// Create a new query request with the given parameters
202    /// Returns the request buffer
203    pub fn create(
204        reply_hwnd: u32,
205        reply_copydata_message: u32,
206        search_flags: u32,
207        offset: u32,
208        max_results: u32,
209        request_flags: u32,
210        sort_type: u32,
211        search: &str,
212    ) -> Vec<u8> {
213        let search_len = search.encode_utf16().count();
214        let total_size = Self::size_for_search(search_len);
215
216        let mut buffer = Vec::with_capacity(total_size);
217
218        // Write the header fields (exactly 28 bytes)
219        buffer.extend_from_slice(&reply_hwnd.to_le_bytes());
220        buffer.extend_from_slice(&reply_copydata_message.to_le_bytes());
221        buffer.extend_from_slice(&search_flags.to_le_bytes());
222        buffer.extend_from_slice(&offset.to_le_bytes());
223        buffer.extend_from_slice(&max_results.to_le_bytes());
224        buffer.extend_from_slice(&request_flags.to_le_bytes());
225        buffer.extend_from_slice(&sort_type.to_le_bytes());
226
227        // Write the search string (UTF-16)
228        // Always write at least 1 null terminator (2 bytes)
229        for ch in search.encode_utf16() {
230            buffer.extend_from_slice(&ch.to_le_bytes());
231        }
232        // Add null terminator
233        buffer.extend_from_slice(&0u16.to_le_bytes());
234
235        buffer
236    }
237}
238
239/// A single query result item
240/**
241## APIs
242[`QueryItem`] only has data type APIs like [`get_str()`](Self::get_str()) instead of
243directly `get_filename()`, etc.
244This is mainly for symmetric APIs: in `query()` you need call `request_flags()` with needed [`RequestFlags`],
245requiring the same [`RequestFlags`] for getting the data will make the code symmetric and eaiser to read.
246
247A fully type-safe API may be added in the future if it won't blow up the compile time.
248*/
249pub struct QueryItem<'a> {
250    request: RequestFlags,
251    data: &'a [u8],
252}
253
254impl std::fmt::Debug for QueryItem<'_> {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        let mut debug = f.debug_struct("QueryItem");
257        debug.field("request", &self.request);
258        for (name, _flag, value) in self.items_with_name() {
259            debug.field(name, &value);
260        }
261        debug.finish()
262    }
263}
264
265impl QueryItem<'_> {
266    pub(crate) fn new<'a>(request: RequestFlags, data: &'a [u8]) -> QueryItem<'a> {
267        QueryItem { request, data }
268    }
269
270    /// Get raw data pointer for a request flag
271    pub fn get_ptr(&self, flag: RequestFlags) -> Option<*const u8> {
272        if (self.request.bits() & flag.bits()) == 0 {
273            return None;
274        }
275
276        let mut offset = 0;
277        for f in [
278            RequestFlags::FileName,
279            RequestFlags::Path,
280            RequestFlags::FullPathAndFileName,
281            RequestFlags::Extension,
282            RequestFlags::Size,
283            RequestFlags::DateCreated,
284            RequestFlags::DateModified,
285            RequestFlags::DateAccessed,
286            RequestFlags::Attributes,
287            RequestFlags::FileListFileName,
288            RequestFlags::RunCount,
289            RequestFlags::DateRun,
290            RequestFlags::DateRecentlyChanged,
291            RequestFlags::HighlightedFileName,
292            RequestFlags::HighlightedPath,
293            RequestFlags::HighlightedFullPathAndFileName,
294        ] {
295            if (self.request.bits() & f.bits()) == 0 {
296                continue;
297            }
298            if f == flag {
299                let ptr = unsafe { self.data.as_ptr().add(offset) };
300                return Some(ptr);
301            }
302            match f.data_type() {
303                RequestDataType::Str => {
304                    let len = unsafe {
305                        self.data
306                            .as_ptr()
307                            .add(offset)
308                            .cast::<u32>()
309                            .read_unaligned()
310                    } as usize;
311                    offset += mem::size_of::<u32>() + (len + 1) * mem::size_of::<u16>();
312                }
313                RequestDataType::Size => {
314                    offset += mem::size_of::<u64>();
315                }
316                RequestDataType::Date => {
317                    offset += mem::size_of::<FILETIME>();
318                }
319                RequestDataType::Dword => {
320                    offset += mem::size_of::<u32>();
321                }
322            }
323        }
324        None
325    }
326
327    /// Get string value for a request flag
328    pub fn get_str(&self, flag: RequestFlags) -> Option<&U16CStr> {
329        let ptr = self.get_ptr(flag)?;
330        let len = unsafe { ptr.cast::<u32>().read_unaligned() } as usize;
331        let str_ptr = unsafe { ptr.add(mem::size_of::<u32>()) as *const u16 };
332        Some(unsafe { U16CStr::from_ptr_unchecked(str_ptr, len) })
333    }
334
335    /// Get string value for a request flag
336    ///
337    /// Prefer [`QueryItem::get_str()`] when possible.
338    pub fn get_string(&self, flag: RequestFlags) -> Option<String> {
339        self.get_str(flag).map(|s| s.to_string_lossy())
340    }
341
342    /// Get size value for a request flag
343    pub fn get_size(&self, flag: RequestFlags) -> Option<u64> {
344        let ptr = self.get_ptr(flag)?;
345        Some(unsafe { ptr.cast::<u64>().read_unaligned() })
346    }
347
348    /// Get date value for a request flag
349    pub fn get_time(&self, flag: RequestFlags) -> Option<FILETIME> {
350        let ptr = self.get_ptr(flag)?;
351        Some(unsafe { ptr.cast::<FILETIME>().read_unaligned() })
352    }
353
354    /// Get dword value for a request flag
355    pub fn get_u32(&self, flag: RequestFlags) -> Option<u32> {
356        let ptr = self.get_ptr(flag)?;
357        Some(unsafe { ptr.cast::<u32>().read_unaligned() })
358    }
359
360    /// Get value for a request flag as a QueryValue enum
361    pub fn get(&self, flag: RequestFlags) -> Option<QueryValue<'_>> {
362        match flag.data_type() {
363            RequestDataType::Str => self.get_str(flag).map(QueryValue::Str),
364            RequestDataType::Size => self.get_size(flag).map(QueryValue::Size),
365            RequestDataType::Date => self.get_time(flag).map(QueryValue::Time),
366            RequestDataType::Dword => self.get_u32(flag).map(QueryValue::U32),
367        }
368    }
369
370    /// Iterator over all requested flags and their values
371    pub fn keys(&self) -> impl Iterator<Item = RequestFlags> {
372        self.request.iter()
373    }
374
375    /// Iterator over all requested flags and their values
376    pub fn items(&self) -> impl Iterator<Item = (RequestFlags, QueryValue<'_>)> {
377        self.request.iter().map(|r| (r, self.get(r).unwrap()))
378    }
379
380    /// Iterator over all requested flags and their values
381    pub fn items_with_name(
382        &self,
383    ) -> impl Iterator<Item = (&'static str, RequestFlags, QueryValue<'_>)> {
384        self.request
385            .iter_names()
386            .map(|(n, r)| (n, r, self.get(r).unwrap()))
387    }
388}
389
390/// Query results from Everything
391/**
392Internally, this is represented as an index array + an item stream.
393*/
394pub struct QueryList {
395    id: u32,
396    data: Box<[u8]>,
397    offset: u32,
398    len: u32,
399    total_len: u32,
400    request_flags: RequestFlags,
401    sort: Sort,
402}
403
404impl std::fmt::Debug for QueryList {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        let mut debug = f.debug_struct("QueryList");
407        debug
408            .field("id", &self.id)
409            .field("len", &self.len)
410            .field("offset", &self.offset)
411            .field("total_len", &self.total_len)
412            .field("request_flags", &self.request_flags)
413            .field("sort", &self.sort);
414
415        let items: Vec<_> = self.iter().collect();
416        debug.field("items", &items).finish()
417    }
418}
419
420impl QueryList {
421    pub(crate) fn new(id: u32, data: Box<[u8]>) -> Self {
422        // Parse header using EVERYTHING_IPC_LIST2 struct
423        let header = unsafe { std::ptr::read(data.as_ptr() as *const EverythingIpcList2) };
424        Self {
425            id,
426            data: data.into(),
427            len: header.numitems,
428            offset: header.offset,
429            total_len: header.totitems,
430            request_flags: RequestFlags::from_bits_truncate(header.request_flags),
431            sort: header.sort_type.try_into().unwrap(),
432        }
433    }
434
435    /// Get the query ID
436    pub fn id(&self) -> u32 {
437        self.id
438    }
439
440    /// Check if results are empty
441    pub fn is_empty(&self) -> bool {
442        self.len() == 0
443    }
444
445    /// Get the offset of the first result
446    pub fn offset(&self) -> u32 {
447        self.offset
448    }
449
450    /// Number of available items
451    #[doc(alias = "available_num")]
452    pub fn len(&self) -> usize {
453        self.len as usize
454    }
455
456    /// Get the number of found items
457    #[doc(alias = "found_num")]
458    pub fn total_len(&self) -> usize {
459        self.total_len as usize
460    }
461
462    /// Get the request flags
463    pub fn request_flags(&self) -> RequestFlags {
464        self.request_flags
465    }
466
467    /// Get the sort type
468    pub fn sort(&self) -> Sort {
469        self.sort
470    }
471
472    /// Get item at index
473    pub fn get(&self, index: usize) -> Option<QueryItem<'_>> {
474        if index >= self.len() {
475            return None;
476        }
477
478        // Header (EVERYTHING_IPC_LIST2) is 20 bytes
479        // Items array starts at offset 20
480        // Each item (EVERYTHING_IPC_ITEM2) is 8 bytes: flags(4) + data_offset(4)
481        // data_offset in EVERYTHING_IPC_ITEM2 is relative to the start of the entire buffer
482
483        // Get item at index using EVERYTHING_IPC_ITEM2 struct
484        let items_offset = mem::size_of::<EverythingIpcList2>();
485        let item_ptr = unsafe { self.data.as_ptr().add(items_offset + index * 8) };
486        let item: EverythingIpcItem2 =
487            unsafe { std::ptr::read(item_ptr as *const EverythingIpcItem2) };
488
489        // data_offset is relative to the start of the entire buffer
490        let data_offset = item.data_offset as usize;
491        let data_ptr = unsafe { self.data.as_ptr().add(data_offset) };
492
493        // Calculate data length from this offset to end of buffer
494        let data_len = self.data.len() - data_offset;
495
496        // Create a slice reference into the shared data (no allocation)
497        let data_slice = unsafe { std::slice::from_raw_parts(data_ptr as *const u8, data_len) };
498
499        Some(QueryItem::new(self.request_flags, data_slice))
500    }
501
502    /// Iterator over all items
503    pub fn iter(&self) -> QueryListIter<'_> {
504        QueryListIter {
505            list: self,
506            index: 0,
507        }
508    }
509}
510
511/// Iterator over query list
512pub struct QueryListIter<'a> {
513    list: &'a QueryList,
514    index: usize,
515}
516
517impl<'a> Iterator for QueryListIter<'a> {
518    type Item = QueryItem<'a>;
519
520    fn next(&mut self) -> Option<Self::Item> {
521        let item = self.list.get(self.index);
522        self.index += 1;
523        item
524    }
525
526    fn size_hint(&self) -> (usize, Option<usize>) {
527        let size = self.list.len().saturating_sub(self.index);
528        (size, Some(size))
529    }
530}
531
532impl<'a> ExactSizeIterator for QueryListIter<'a> {}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn search_flags_combinations() {
540        let flags = SearchFlags::MatchCase | SearchFlags::MatchWholeWord;
541        assert_eq!(flags.bits(), 0x00000003);
542    }
543
544    #[test]
545    fn request_flags_combinations() {
546        let flags = RequestFlags::FileName | RequestFlags::Size | RequestFlags::DateModified;
547        assert_eq!(flags.bits(), 0x00000051); // 1 | 16 | 64 = 81 = 0x51
548    }
549
550    #[test]
551    fn request_flags_data_type() {
552        assert_eq!(RequestFlags::FileName.data_type(), RequestDataType::Str);
553        assert_eq!(RequestFlags::Size.data_type(), RequestDataType::Size);
554        assert_eq!(RequestFlags::DateCreated.data_type(), RequestDataType::Date);
555        assert_eq!(RequestFlags::Attributes.data_type(), RequestDataType::Dword);
556    }
557
558    #[test]
559    fn sort_default() {
560        assert_eq!(Sort::default(), Sort::NameAscending);
561    }
562
563    #[test]
564    fn version_default() {
565        let version: crate::Version = Default::default();
566        assert_eq!(version.major, 0);
567        assert_eq!(version.minor, 0);
568        assert_eq!(version.revision, 0);
569        assert_eq!(version.build, 0);
570    }
571
572    #[test]
573    fn search_flags_not() {
574        let flags = !SearchFlags::MatchCase;
575        // bitflags::Not only inverts the defined bits, not the entire u32
576        // For SearchFlags with 5 bits defined (0x1F), !MATCH_CASE = 0x1E = 30
577        assert_eq!(flags.bits(), 0x1E);
578    }
579
580    #[test]
581    fn request_flags_not() {
582        let flags = !RequestFlags::FileName;
583        // bitflags::Not only inverts the defined bits, not the entire u32
584        // For RequestFlags with 16 bits defined (0xFFFF), !FILE_NAME = 0xFFFE = 65534
585        assert_eq!(flags.bits(), 0xFFFE);
586    }
587
588    #[test]
589    fn everything_ipc_query2_size() {
590        // Test the struct size calculation
591        // Header is exactly 28 bytes (7 DWORDs) + minimum 2 bytes for search_string
592
593        // Test size_for_search with empty string (just the minimum 1 u16)
594        assert_eq!(EverythingIpcQuery2::size_for_search(0), 30); // 30 bytes minimum
595
596        // Test size_for_search with "test" (4 chars)
597        let search_size = 30 + 4 * 2; // 30 minimum + 4 chars * 2 bytes per char
598        assert_eq!(EverythingIpcQuery2::size_for_search(4), search_size);
599    }
600
601    #[test]
602    fn everything_ipc_query2_create() {
603        let buffer = EverythingIpcQuery2::create(
604            1234,
605            5678,
606            0,
607            0,
608            10,
609            RequestFlags::FileName.bits(),
610            Sort::NameAscending as u32,
611            "test",
612        );
613
614        // Minimum 30 bytes + 4 chars * 2 bytes = 38 bytes (30 = 28 header + 2 null terminator)
615        assert_eq!(buffer.len(), 38);
616
617        // Verify header fields
618        assert_eq!(u32::from_le_bytes(buffer[0..4].try_into().unwrap()), 1234);
619        assert_eq!(u32::from_le_bytes(buffer[4..8].try_into().unwrap()), 5678);
620        assert_eq!(u32::from_le_bytes(buffer[8..12].try_into().unwrap()), 0);
621        assert_eq!(u32::from_le_bytes(buffer[12..16].try_into().unwrap()), 0);
622        assert_eq!(u32::from_le_bytes(buffer[16..20].try_into().unwrap()), 10);
623        assert_eq!(
624            u32::from_le_bytes(buffer[20..24].try_into().unwrap()),
625            RequestFlags::FileName.bits()
626        );
627        assert_eq!(
628            u32::from_le_bytes(buffer[24..28].try_into().unwrap()),
629            Sort::NameAscending as u32
630        );
631    }
632
633    // Helper to create minimal QueryList data
634    // Note: EVERYTHING_IPC_LIST2 header is 20 bytes:
635    //   - totitems (4) + numitems (4) + offset (4) + request_flags (4) + sort_type (4)
636    fn make_query_data(
637        found_num: u32,
638        available_num: u32,
639        request_flags: u32,
640        sort: Sort,
641        items: &[(u32, u32)],
642        data: &[u8],
643    ) -> Box<[u8]> {
644        let mut result = Vec::new();
645        result.extend_from_slice(&found_num.to_le_bytes());
646        result.extend_from_slice(&available_num.to_le_bytes());
647        result.extend_from_slice(&0u32.to_le_bytes()); // offset field (unused)
648        result.extend_from_slice(&request_flags.to_le_bytes());
649        result.extend_from_slice(&(sort as u32).to_le_bytes());
650
651        // Write items
652        for (flags, offset) in items {
653            result.extend_from_slice(&flags.to_le_bytes());
654            result.extend_from_slice(&offset.to_le_bytes());
655        }
656
657        // Write data
658        result.extend_from_slice(data);
659        result.into_boxed_slice()
660    }
661
662    #[test]
663    fn query_list_empty() {
664        // Create minimal valid data with zero available items
665        // Header (EVERYTHING_IPC_LIST2): found_num(4) + available_num(4) + offset(4) + request_flags(4) + sort(4) = 20 bytes
666        let data = make_query_data(0, 0, 0, Sort::default(), &[], b"");
667        let results = QueryList::new(0, data);
668        assert!(results.is_empty());
669        assert_eq!(results.len(), 0);
670        assert_eq!(results.total_len(), 0);
671        assert!(results.get(0).is_none());
672    }
673
674    #[test]
675    fn query_list_with_items() {
676        // Create a query list with one file name
677        // Header (EVERYTHING_IPC_LIST2): 20 bytes
678        // Item (EVERYTHING_IPC_ITEM2): flags(4) + offset(4) = 8 bytes
679
680        // For a simple test, we just verify the structure parsing works
681        let data = make_query_data(
682            1,
683            1,
684            RequestFlags::FileName.bits(),
685            Sort::NameAscending,
686            &[(RequestFlags::FileName.bits(), 20)], // offset 20 = start of data
687            b"",
688        );
689        let results = QueryList::new(0, data);
690        assert_eq!(results.total_len(), 1);
691        assert_eq!(results.len(), 1);
692        assert_eq!(results.request_flags(), RequestFlags::FileName);
693    }
694
695    #[test]
696    fn query_list_multiple_items() {
697        let data = make_query_data(
698            2,
699            2,
700            RequestFlags::FileName.bits(),
701            Sort::NameAscending,
702            &[
703                (RequestFlags::FileName.bits(), 20), // offset 20 = start of data
704                (RequestFlags::FileName.bits(), 28), // offset 28 = 20 + 8 (8 bytes for first item)
705            ],
706            b"",
707        );
708        let results = QueryList::new(0, data);
709        assert_eq!(results.total_len(), 2);
710        assert_eq!(results.len(), 2);
711    }
712
713    #[test]
714    fn query_list_sort_parsing() {
715        for (sort_value, expected_sort) in [
716            (1, Sort::NameAscending),
717            (2, Sort::NameDescending),
718            (3, Sort::PathAscending),
719            (4, Sort::PathDescending),
720            (5, Sort::SizeAscending),
721            (6, Sort::SizeDescending),
722            (7, Sort::ExtensionAscending),
723            (8, Sort::ExtensionDescending),
724        ] {
725            let data = make_query_data(0, 0, 0, expected_sort, &[], b"");
726            let results = QueryList::new(0, data);
727            assert_eq!(results.sort(), expected_sort, "sort value {}", sort_value);
728        }
729    }
730
731    #[test]
732    fn query_item_str() {
733        // Create data with a UTF-16 string "test"
734        // Header is 20 bytes, items array is 8 bytes (1 item)
735        // So data starts at offset 20 + 8 = 28
736        // Format: length (u32 LE) + string (u16 LE) + null terminator
737        // "test" in UTF-16: [116, 0, 101, 0, 115, 0, 116, 0] (4 chars)
738        // Plus null terminator = 5 u16s = 10 bytes
739        // String data: length(4) + string(10) = 14 bytes
740        let string_data: Vec<u8> = vec![4, 0, 0, 0, 116, 0, 101, 0, 115, 0, 116, 0, 0, 0];
741        let data = make_query_data(
742            1,
743            1,
744            RequestFlags::FileName.bits(),
745            Sort::NameAscending,
746            &[(RequestFlags::FileName.bits(), 28)], // offset 28 = 20 (header) + 8 (1 item)
747            &string_data,
748        );
749        let results = QueryList::new(0, data);
750
751        // Get the first item
752        if let Some(item) = results.get(0) {
753            if let Some(str_val) = item.get_str(RequestFlags::FileName) {
754                assert_eq!(str_val.to_string_lossy(), "test");
755            } else {
756                panic!("Expected FILE_NAME string");
757            }
758        } else {
759            panic!("Expected item at index 0");
760        }
761    }
762
763    #[test]
764    fn query_list_iter() {
765        let data = make_query_data(
766            2,
767            2,
768            RequestFlags::FileName.bits(),
769            Sort::NameAscending,
770            &[
771                (RequestFlags::FileName.bits(), 20), // offset 20 = start of data
772                (RequestFlags::FileName.bits(), 28), // offset 28 = 20 + 8 (8 bytes for first item)
773            ],
774            b"",
775        );
776        let results = QueryList::new(0, data);
777
778        let mut count = 0;
779        for _ in results.iter() {
780            count += 1;
781        }
782        assert_eq!(count, 2);
783    }
784
785    #[test]
786    fn query_item_out_of_bounds() {
787        let data = make_query_data(0, 0, 0, Sort::default(), &[], b"");
788        let results = QueryList::new(0, data);
789        assert!(results.get(0).is_none());
790    }
791
792    #[test]
793    fn filetime_structure() {
794        let ft = FILETIME {
795            dwLowDateTime: 0x12345678,
796            dwHighDateTime: 0x9ABCDEF0,
797        };
798        assert_eq!(ft.dwLowDateTime, 0x12345678);
799        assert_eq!(ft.dwHighDateTime, 0x9ABCDEF0);
800    }
801
802    #[test]
803    fn query_list_get_by_index() {
804        let data = make_query_data(
805            1,
806            1,
807            RequestFlags::FileName.bits(),
808            Sort::NameAscending,
809            &[(RequestFlags::FileName.bits(), 20)], // offset 20 = start of data
810            b"",
811        );
812        let results = QueryList::new(0, data);
813
814        assert!(results.get(0).is_some());
815        assert!(results.get(1).is_none()); // Out of bounds
816    }
817
818    #[test]
819    fn request_flags_all() {
820        // Test all request flags have unique values
821        let flags = [
822            RequestFlags::FileName,
823            RequestFlags::Path,
824            RequestFlags::FullPathAndFileName,
825            RequestFlags::Extension,
826            RequestFlags::Size,
827            RequestFlags::DateCreated,
828            RequestFlags::DateModified,
829            RequestFlags::DateAccessed,
830            RequestFlags::Attributes,
831            RequestFlags::FileListFileName,
832            RequestFlags::RunCount,
833            RequestFlags::DateRun,
834            RequestFlags::DateRecentlyChanged,
835            RequestFlags::HighlightedFileName,
836            RequestFlags::HighlightedPath,
837            RequestFlags::HighlightedFullPathAndFileName,
838        ];
839
840        let mut seen = std::collections::HashSet::new();
841        for flag in flags {
842            assert!(
843                !seen.contains(&flag.0),
844                "Duplicate flag value: {:#x}",
845                flag.0
846            );
847            seen.insert(flag.0);
848        }
849    }
850}