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;