Skip to main content

windows_file_enumeration_sys/
entry.rs

1// Copyright (c) 2026 Mike Grier
2//! What one enumerated directory entry carries.
3//!
4//! Every field a `FILE_ID_EXTD_DIR_INFO` record supplies inline is present on
5//! every entry, in the unit the record reported it. That is not generosity: the
6//! record pays for all of them in the same query, so making any of them optional
7//! would save no native work while narrowing the platform.
8//!
9//! The one thing that *is* selectable is volume qualification
10//! ([`FileIdentityMode`]), because obtaining a volume serial needs a second
11//! query against the directory handle.
12//!
13//! `FileIndex` is deliberately absent. Windows documents it as undefined for
14//! filesystems including NTFS, so exposing it would invite callers to depend on
15//! a value with no meaning.
16
17use wtf_string::{Wtf16Str, Wtf16String};
18
19use crate::WindowsFileTimestamp;
20
21/// Whether an entry is a directory or an ordinary file.
22///
23/// This is closed rather than extensible because Windows decides it with one
24/// attribute bit: an entry either has `FILE_ATTRIBUTE_DIRECTORY` or it does not.
25/// Anything finer -- a reparse point, a device, an offline file -- is a property
26/// of the raw attributes, which every entry also carries.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum EntryType {
29    /// The entry is not a directory.
30    File,
31    /// The entry is a directory.
32    Directory,
33}
34
35/// A filesystem object's identity: the record's 128-bit file ID, optionally
36/// qualified by the volume it lives on.
37///
38/// A file ID is unique only *within* a volume, so the same ID may name different
39/// objects on different volumes. An unqualified identity is therefore not
40/// globally meaningful and must not be compared across volumes;
41/// [`is_volume_qualified`](Self::is_volume_qualified) reports which kind this is.
42///
43/// The 16 identifier bytes are kept exactly as the record reported them. They
44/// are deliberately not folded into a `u128`, which would impose an endianness
45/// the native value does not have.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct FileIdentity {
48    id: [u8; 16],
49    volume_serial: Option<u64>,
50}
51
52impl FileIdentity {
53    /// Build an identity from a record's identifier bytes and, when it was
54    /// obtained, the volume serial that qualifies them.
55    #[must_use]
56    pub const fn new(id: [u8; 16], volume_serial: Option<u64>) -> Self {
57        Self { id, volume_serial }
58    }
59
60    /// The record's 16 identifier bytes, verbatim.
61    #[must_use]
62    pub const fn id_bytes(&self) -> [u8; 16] {
63        self.id
64    }
65
66    /// The volume serial, when the request obtained one.
67    #[must_use]
68    pub const fn volume_serial(&self) -> Option<u64> {
69        self.volume_serial
70    }
71
72    /// Whether this identity is globally meaningful.
73    #[must_use]
74    pub const fn is_volume_qualified(&self) -> bool {
75        self.volume_serial.is_some()
76    }
77}
78
79/// How much work a request is willing to do for file identity.
80///
81/// The 128-bit file ID is inline in every record and always present. Only the
82/// volume serial that qualifies it costs an extra query, and that query runs
83/// once against the directory handle -- no mode ever opens an individual entry.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
85pub enum FileIdentityMode {
86    /// Do not query the volume serial. Entries carry unqualified identities.
87    ///
88    /// The default, because a caller that never compares identities across
89    /// volumes should not pay for the query.
90    #[default]
91    Omit,
92    /// Query the volume serial once, and carry on without it if that fails.
93    ///
94    /// Entries then carry unqualified identities rather than failing the
95    /// enumeration -- the shape a caller wants when identity refines its work
96    /// but does not gate it.
97    BestEffort,
98    /// Query the volume serial once, and fail the enumeration before its first
99    /// entry if it cannot be obtained.
100    ///
101    /// Use this when an unqualified identity would be silently wrong.
102    Required,
103}
104
105impl FileIdentityMode {
106    /// Whether this mode performs the volume-serial query at all.
107    #[must_use]
108    pub const fn queries_volume(self) -> bool {
109        matches!(
110            self,
111            FileIdentityMode::BestEffort | FileIdentityMode::Required
112        )
113    }
114}
115
116/// One enumerated directory entry with its full inline metadata.
117///
118/// Names are the entry's own leaf name -- never a path -- and stay native-width
119/// WTF-16, so an ill-formed surrogate a filesystem happens to contain survives
120/// the round trip rather than being replaced.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct DirectoryEntry {
123    name: Wtf16String,
124    attributes: u32,
125    reparse_tag: Option<u32>,
126    logical_size: u64,
127    allocation_size: u64,
128    extended_attribute_size: u32,
129    creation_time: WindowsFileTimestamp,
130    last_access_time: WindowsFileTimestamp,
131    last_write_time: WindowsFileTimestamp,
132    change_time: WindowsFileTimestamp,
133    identity: FileIdentity,
134}
135
136/// The parsed field values of one native record, before they become a
137/// [`DirectoryEntry`].
138///
139/// This exists so the native engine can hand over a dozen values without a
140/// dozen positional arguments, and so adding a field later is not a breaking
141/// change to a constructor. It is crate-internal: the public surface is
142/// [`DirectoryEntry`]'s accessors.
143pub(crate) struct EntryFields {
144    pub(crate) name: Wtf16String,
145    pub(crate) attributes: u32,
146    pub(crate) logical_size: u64,
147    pub(crate) allocation_size: u64,
148    pub(crate) extended_attribute_size: u32,
149    pub(crate) creation_time: WindowsFileTimestamp,
150    pub(crate) last_access_time: WindowsFileTimestamp,
151    pub(crate) last_write_time: WindowsFileTimestamp,
152    pub(crate) change_time: WindowsFileTimestamp,
153    pub(crate) reparse_tag: u32,
154    pub(crate) identity: FileIdentity,
155}
156
157/// `FILE_ATTRIBUTE_DIRECTORY`, the single bit that decides [`EntryType`].
158const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
159
160/// `FILE_ATTRIBUTE_REPARSE_POINT`, the single bit that decides whether the
161/// record's reparse tag means anything.
162const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
163
164impl DirectoryEntry {
165    /// Build an entry from one record's parsed fields.
166    ///
167    /// The reparse tag is admitted only when the attributes say the entry is a
168    /// reparse point. A record's tag field is otherwise meaningless, and
169    /// surfacing it would let a caller act on a tag that names nothing.
170    #[must_use]
171    pub(crate) fn from_fields(fields: EntryFields) -> Self {
172        let reparse_tag =
173            (fields.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0).then_some(fields.reparse_tag);
174        Self {
175            name: fields.name,
176            attributes: fields.attributes,
177            reparse_tag,
178            logical_size: fields.logical_size,
179            allocation_size: fields.allocation_size,
180            extended_attribute_size: fields.extended_attribute_size,
181            creation_time: fields.creation_time,
182            last_access_time: fields.last_access_time,
183            last_write_time: fields.last_write_time,
184            change_time: fields.change_time,
185            identity: fields.identity,
186        }
187    }
188
189    /// The entry's own leaf name, in native WTF-16.
190    #[must_use]
191    pub fn name(&self) -> &Wtf16Str {
192        &self.name
193    }
194
195    /// Take ownership of the name, consuming the entry.
196    #[must_use]
197    pub fn into_name(self) -> Wtf16String {
198        self.name
199    }
200
201    /// Whether the entry is a directory or a file.
202    #[must_use]
203    pub const fn entry_type(&self) -> EntryType {
204        if self.attributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
205            EntryType::Directory
206        } else {
207            EntryType::File
208        }
209    }
210
211    /// The raw `FILE_ATTRIBUTE_*` bitmask, exactly as the record reported it.
212    #[must_use]
213    pub const fn attributes(&self) -> u32 {
214        self.attributes
215    }
216
217    /// Whether the entry is a reparse point.
218    #[must_use]
219    pub const fn is_reparse_point(&self) -> bool {
220        self.reparse_tag.is_some()
221    }
222
223    /// The reparse tag, present exactly when the entry is a reparse point.
224    #[must_use]
225    pub const fn reparse_tag(&self) -> Option<u32> {
226        self.reparse_tag
227    }
228
229    /// The end-of-file offset in bytes: how much data the entry holds.
230    #[must_use]
231    pub const fn logical_size(&self) -> u64 {
232        self.logical_size
233    }
234
235    /// The bytes allocated on the volume, which may exceed or -- for a
236    /// compressed or sparse file -- fall short of the logical size.
237    #[must_use]
238    pub const fn allocation_size(&self) -> u64 {
239        self.allocation_size
240    }
241
242    /// The size of the entry's extended attributes, in bytes.
243    #[must_use]
244    pub const fn extended_attribute_size(&self) -> u32 {
245        self.extended_attribute_size
246    }
247
248    /// When the entry was created.
249    #[must_use]
250    pub const fn creation_time(&self) -> WindowsFileTimestamp {
251        self.creation_time
252    }
253
254    /// When the entry was last accessed.
255    #[must_use]
256    pub const fn last_access_time(&self) -> WindowsFileTimestamp {
257        self.last_access_time
258    }
259
260    /// When the entry's data was last written.
261    #[must_use]
262    pub const fn last_write_time(&self) -> WindowsFileTimestamp {
263        self.last_write_time
264    }
265
266    /// When the entry's metadata last changed.
267    ///
268    /// This has no `WIN32_FIND_DATAW` equivalent; it is one of the fields that
269    /// makes the extended directory-information class worth requiring.
270    #[must_use]
271    pub const fn change_time(&self) -> WindowsFileTimestamp {
272        self.change_time
273    }
274
275    /// The entry's identity, volume-qualified only if the request asked for it.
276    #[must_use]
277    pub const fn identity(&self) -> FileIdentity {
278        self.identity
279    }
280}
281
282#[cfg(test)]
283mod tests;