Skip to main content

miri/shims/windows/
fs.rs

1use std::fs::{self, Dir};
2use std::io;
3use std::io::SeekFrom;
4use std::time::SystemTime;
5
6use bitflags::bitflags;
7use rustc_abi::Size;
8use rustc_target::spec::Os;
9
10use crate::shims::files::{DirHandle, FileHandle};
11use crate::shims::windows::handle::{EvalContextExt as _, Handle};
12use crate::*;
13
14#[derive(Copy, Clone, Debug, PartialEq)]
15enum CreationDisposition {
16    CreateAlways,
17    CreateNew,
18    OpenAlways,
19    OpenExisting,
20    TruncateExisting,
21}
22
23impl CreationDisposition {
24    fn new<'tcx>(
25        value: u32,
26        ecx: &mut MiriInterpCx<'tcx>,
27    ) -> InterpResult<'tcx, CreationDisposition> {
28        let create_always = ecx.eval_windows_u32("c", "CREATE_ALWAYS");
29        let create_new = ecx.eval_windows_u32("c", "CREATE_NEW");
30        let open_always = ecx.eval_windows_u32("c", "OPEN_ALWAYS");
31        let open_existing = ecx.eval_windows_u32("c", "OPEN_EXISTING");
32        let truncate_existing = ecx.eval_windows_u32("c", "TRUNCATE_EXISTING");
33
34        let out = if value == create_always {
35            CreationDisposition::CreateAlways
36        } else if value == create_new {
37            CreationDisposition::CreateNew
38        } else if value == open_always {
39            CreationDisposition::OpenAlways
40        } else if value == open_existing {
41            CreationDisposition::OpenExisting
42        } else if value == truncate_existing {
43            CreationDisposition::TruncateExisting
44        } else {
45            throw_unsup_format!("CreateFileW: Unsupported creation disposition: {value}");
46        };
47        interp_ok(out)
48    }
49}
50
51bitflags! {
52    #[derive(PartialEq)]
53    struct FileAttributes: u32 {
54        const ZERO = 0;
55        const NORMAL = 1 << 0;
56        /// This must be passed to allow getting directory handles. If not passed, we error on trying
57        /// to open directories
58        const BACKUP_SEMANTICS = 1 << 1;
59        /// Open a reparse point as a regular file - this is basically similar to 'readlink' in Unix
60        /// terminology. A reparse point is a file with custom logic when navigated to, of which
61        /// a symlink is one specific example.
62        const OPEN_REPARSE = 1 << 2;
63    }
64}
65
66impl FileAttributes {
67    fn new<'tcx>(
68        mut value: u32,
69        ecx: &mut MiriInterpCx<'tcx>,
70    ) -> InterpResult<'tcx, FileAttributes> {
71        let file_attribute_normal = ecx.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL");
72        let file_flag_backup_semantics = ecx.eval_windows_u32("c", "FILE_FLAG_BACKUP_SEMANTICS");
73        let file_flag_open_reparse_point =
74            ecx.eval_windows_u32("c", "FILE_FLAG_OPEN_REPARSE_POINT");
75
76        let mut out = FileAttributes::ZERO;
77        if value & file_flag_backup_semantics != 0 {
78            value &= !file_flag_backup_semantics;
79            out |= FileAttributes::BACKUP_SEMANTICS;
80        }
81        if value & file_flag_open_reparse_point != 0 {
82            value &= !file_flag_open_reparse_point;
83            out |= FileAttributes::OPEN_REPARSE;
84        }
85        if value & file_attribute_normal != 0 {
86            value &= !file_attribute_normal;
87            out |= FileAttributes::NORMAL;
88        }
89
90        if value != 0 {
91            throw_unsup_format!("CreateFileW: Unsupported flags_and_attributes: {value}");
92        }
93
94        if out == FileAttributes::ZERO {
95            // NORMAL is equivalent to 0. Avoid needing to check both cases by unifying the two.
96            out = FileAttributes::NORMAL;
97        }
98        interp_ok(out)
99    }
100}
101
102impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
103#[allow(non_snake_case)]
104pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
105    fn CreateFileW(
106        &mut self,
107        file_name: &OpTy<'tcx>,            // LPCWSTR
108        desired_access: &OpTy<'tcx>,       // DWORD
109        share_mode: &OpTy<'tcx>,           // DWORD
110        security_attributes: &OpTy<'tcx>,  // LPSECURITY_ATTRIBUTES
111        creation_disposition: &OpTy<'tcx>, // DWORD
112        flags_and_attributes: &OpTy<'tcx>, // DWORD
113        template_file: &OpTy<'tcx>,        // HANDLE
114    ) -> InterpResult<'tcx, Handle> {
115        // ^ Returns HANDLE
116        use CreationDisposition::*;
117
118        let this = self.eval_context_mut();
119        this.assert_target_os(Os::Windows, "CreateFileW");
120        this.check_no_isolation("`CreateFileW`")?;
121
122        // This function appears to always set the error to 0. This is important for some flag
123        // combinations, which may set error code on success.
124        this.set_last_error(IoError::Raw(Scalar::from_i32(0)))?;
125
126        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
127        let mut desired_access = this.read_scalar(desired_access)?.to_u32()?;
128        let share_mode = this.read_scalar(share_mode)?.to_u32()?;
129        let security_attributes = this.read_pointer(security_attributes)?;
130        let creation_disposition = this.read_scalar(creation_disposition)?.to_u32()?;
131        let flags_and_attributes = this.read_scalar(flags_and_attributes)?.to_u32()?;
132        let template_file = this.read_target_usize(template_file)?;
133
134        let generic_read = this.eval_windows_u32("c", "GENERIC_READ");
135        let generic_write = this.eval_windows_u32("c", "GENERIC_WRITE");
136
137        let file_share_delete = this.eval_windows_u32("c", "FILE_SHARE_DELETE");
138        let file_share_read = this.eval_windows_u32("c", "FILE_SHARE_READ");
139        let file_share_write = this.eval_windows_u32("c", "FILE_SHARE_WRITE");
140
141        let creation_disposition = CreationDisposition::new(creation_disposition, this)?;
142        let attributes = FileAttributes::new(flags_and_attributes, this)?;
143
144        if share_mode != (file_share_delete | file_share_read | file_share_write) {
145            throw_unsup_format!("CreateFileW: Unsupported share mode: {share_mode}");
146        }
147        if !this.ptr_is_null(security_attributes)? {
148            throw_unsup_format!("CreateFileW: Security attributes are not supported");
149        }
150
151        if attributes.contains(FileAttributes::OPEN_REPARSE) && creation_disposition == CreateAlways
152        {
153            throw_machine_stop!(TerminationInfo::Abort("Invalid CreateFileW argument combination: FILE_FLAG_OPEN_REPARSE_POINT with CREATE_ALWAYS".to_string()));
154        }
155
156        if template_file != 0 {
157            throw_unsup_format!("CreateFileW: Template files are not supported");
158        }
159
160        // We need to know if the file is a directory to correctly open directory handles. This is
161        // racy, but currently the stdlib doesn't appear to offer a better solution. We do later
162        // verify that our guess was correct so worst-case, Miri ICEs here.
163        // FIXME: retry in a loop if we get an error indicating we got the wrong file type?
164        let is_dir = file_name.is_dir();
165
166        // BACKUP_SEMANTICS is how Windows calls the act of opening a directory handle.
167        if !attributes.contains(FileAttributes::BACKUP_SEMANTICS) && is_dir {
168            this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
169            return interp_ok(Handle::Invalid);
170        }
171
172        let desired_read = desired_access & generic_read != 0;
173        let desired_write = desired_access & generic_write != 0;
174
175        let mut options = fs::OpenOptions::new();
176        if desired_read {
177            desired_access &= !generic_read;
178            options.read(true);
179        }
180        if desired_write {
181            desired_access &= !generic_write;
182            options.write(true);
183        }
184
185        if desired_access != 0 {
186            throw_unsup_format!(
187                "CreateFileW: Unsupported bits set for access mode: {desired_access:#x}"
188            );
189        }
190
191        // Per the documentation:
192        // If the specified file exists and is writable, the function truncates the file,
193        // the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS.
194        // If the specified file does not exist and is a valid path, a new file is created,
195        // the function succeeds, and the last-error code is set to zero.
196        // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
197        //
198        // This is racy, but there doesn't appear to be an std API that both succeeds if a
199        // file exists but tells us it isn't new. Either we accept racing one way or another,
200        // or we use an iffy heuristic like file creation time. This implementation prefers
201        // to fail in the direction of erroring more often.
202        if let CreateAlways | OpenAlways = creation_disposition
203            && file_name.exists()
204        {
205            this.set_last_error(IoError::WindowsError("ERROR_ALREADY_EXISTS"))?;
206        }
207
208        let handle = if is_dir {
209            // Open this as a directory.
210            // FIXME: shouldn't we check `creation_disposition` here?
211            Dir::open(&file_name).map(|dir| {
212                #[cfg(not(bootstrap))]
213                assert!(
214                    dir.metadata().unwrap().is_dir(),
215                    "we tried to open a directory and got a file"
216                );
217                let fd_num = this.machine.fds.insert_new(DirHandle { dir, path: file_name });
218                Handle::File(fd_num)
219            })
220        } else {
221            // Open this as a standard file. We already set the `read`/`write` flags above,
222            // but we still need to represent the `creation_disposition`.
223            match creation_disposition {
224                CreateAlways | OpenAlways => {
225                    options.create(true);
226                    if creation_disposition == CreateAlways {
227                        options.truncate(true);
228                    }
229                }
230                CreateNew => {
231                    options.create_new(true);
232                    // Per `create_new` documentation:
233                    // The file must be opened with write or append access in order to create a new file.
234                    // https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
235                    if !desired_write {
236                        options.append(true);
237                    }
238                }
239                OpenExisting => {
240                    if !desired_read && !desired_write {
241                        // Windows supports handles with no permissions. These allow things such as
242                        // reading metadata, but not file content. This is used by `Path::metadata`.
243                        // `std` does not support this. To ensure we behave correctly as often as
244                        // possible, we open the file for reading and live with the fact that this
245                        // might incorrectly return `PermissionDenied`.
246                        // FIXME: We could probably use `OpenOptionsExt`? On a Unix host,
247                        // `O_PATH` apparently can open files for metadata use only.
248                        options.read(true);
249                    }
250                }
251                TruncateExisting => {
252                    options.truncate(true);
253                }
254            }
255
256            options.open(file_name).map(|file| {
257                assert!(
258                    !file.metadata().unwrap().is_dir(),
259                    "we tried to open a file and got a directory"
260                );
261                let fd_num = this.machine.fds.insert_new(FileHandle {
262                    file,
263                    writable: desired_write,
264                    readable: desired_read,
265                });
266                Handle::File(fd_num)
267            })
268        };
269
270        match handle {
271            Ok(handle) => interp_ok(handle),
272            Err(e) => {
273                this.set_last_error(e)?;
274                interp_ok(Handle::Invalid)
275            }
276        }
277    }
278
279    fn GetFileInformationByHandle(
280        &mut self,
281        file: &OpTy<'tcx>,             // HANDLE
282        file_information: &OpTy<'tcx>, // LPBY_HANDLE_FILE_INFORMATION
283    ) -> InterpResult<'tcx, Scalar> {
284        // ^ Returns BOOL (i32 on Windows)
285        let this = self.eval_context_mut();
286        this.assert_target_os(Os::Windows, "GetFileInformationByHandle");
287        this.check_no_isolation("`GetFileInformationByHandle`")?;
288
289        let file = this.read_handle(file, "GetFileInformationByHandle")?;
290        let file_information = this.deref_pointer_as(
291            file_information,
292            this.windows_ty_layout("BY_HANDLE_FILE_INFORMATION"),
293        )?;
294
295        let Handle::File(fd_num) = file else { this.invalid_handle("GetFileInformationByHandle")? };
296
297        let Some(desc) = this.machine.fds.get(fd_num) else {
298            this.invalid_handle("GetFileInformationByHandle")?
299        };
300
301        let metadata = match desc.metadata()? {
302            Either::Left(Ok(meta)) => meta,
303            Either::Left(Err(e)) => {
304                this.set_last_error(e)?;
305                return interp_ok(this.eval_windows("c", "FALSE"));
306            }
307            Either::Right(_mode) =>
308                throw_unsup_format!(
309                    "`GetFileInformationByHandle` is not supported on non-file-backed handles"
310                ),
311        };
312
313        let size = metadata.len();
314
315        let file_type = metadata.file_type();
316        let attributes = if file_type.is_dir() {
317            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DIRECTORY")
318        } else if file_type.is_file() {
319            this.eval_windows_u32("c", "FILE_ATTRIBUTE_NORMAL")
320        } else {
321            this.eval_windows_u32("c", "FILE_ATTRIBUTE_DEVICE")
322        };
323
324        // Per the Windows documentation:
325        // "If the underlying file system does not support the [...] time, this member is zero (0)."
326        // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
327        let created = extract_windows_epoch(this, metadata.created())?.unwrap_or((0, 0));
328        let accessed = extract_windows_epoch(this, metadata.accessed())?.unwrap_or((0, 0));
329        let written = extract_windows_epoch(this, metadata.modified())?.unwrap_or((0, 0));
330
331        this.write_int_fields_named(&[("dwFileAttributes", attributes.into())], &file_information)?;
332        write_filetime_field(this, &file_information, "ftCreationTime", created)?;
333        write_filetime_field(this, &file_information, "ftLastAccessTime", accessed)?;
334        write_filetime_field(this, &file_information, "ftLastWriteTime", written)?;
335        this.write_int_fields_named(
336            &[
337                ("dwVolumeSerialNumber", 0),
338                ("nFileSizeHigh", (size >> 32).into()),
339                ("nFileSizeLow", (size & 0xFFFFFFFF).into()),
340                ("nNumberOfLinks", 1),
341                ("nFileIndexHigh", 0),
342                ("nFileIndexLow", 0),
343            ],
344            &file_information,
345        )?;
346
347        interp_ok(this.eval_windows("c", "TRUE"))
348    }
349
350    fn SetFileInformationByHandle(
351        &mut self,
352        file: &OpTy<'tcx>,             // HANDLE
353        class: &OpTy<'tcx>,            // FILE_INFO_BY_HANDLE_CLASS
354        file_information: &OpTy<'tcx>, // LPVOID
355        buffer_size: &OpTy<'tcx>,      // DWORD
356    ) -> InterpResult<'tcx, Scalar> {
357        // ^ Returns BOOL (i32 on Windows)
358        let this = self.eval_context_mut();
359        this.assert_target_os(Os::Windows, "SetFileInformationByHandle");
360        this.check_no_isolation("`SetFileInformationByHandle`")?;
361
362        let class = this.read_scalar(class)?.to_u32()?;
363        let buffer_size = this.read_scalar(buffer_size)?.to_u32()?;
364        let file_information = this.read_pointer(file_information)?;
365        this.check_ptr_access(
366            file_information,
367            Size::from_bytes(buffer_size),
368            CheckInAllocMsg::MemoryAccess,
369        )?;
370
371        let file = this.read_handle(file, "SetFileInformationByHandle")?;
372        let Handle::File(fd_num) = file else { this.invalid_handle("SetFileInformationByHandle")? };
373        let Some(desc) = this.machine.fds.get(fd_num) else {
374            this.invalid_handle("SetFileInformationByHandle")?
375        };
376        let file = desc.downcast::<FileHandle>().ok_or_else(|| {
377            err_unsup_format!(
378                "`SetFileInformationByHandle` is only supported on file-backed file descriptors"
379            )
380        })?;
381
382        if class == this.eval_windows_u32("c", "FileEndOfFileInfo") {
383            let place = this
384                .ptr_to_mplace(file_information, this.windows_ty_layout("FILE_END_OF_FILE_INFO"));
385            let new_len =
386                this.read_scalar(&this.project_field_named(&place, "EndOfFile")?)?.to_i64()?;
387            match file.file.set_len(new_len.try_into().unwrap()) {
388                Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
389                Err(e) => {
390                    this.set_last_error(e)?;
391                    interp_ok(this.eval_windows("c", "FALSE"))
392                }
393            }
394        } else if class == this.eval_windows_u32("c", "FileAllocationInfo") {
395            // On Windows, files are somewhat similar to a `Vec` in that they have a separate
396            // "length" (called "EOF position") and "capacity" (called "allocation size").
397            // Growing the allocation size is largely a performance hint which we can
398            // ignore -- it can also be directly queried, but we currently do not support that.
399            // So we only need to do something if this operation shrinks the allocation size
400            // so far that it affects the EOF position.
401            let place = this
402                .ptr_to_mplace(file_information, this.windows_ty_layout("FILE_ALLOCATION_INFO"));
403            let new_alloc_size: u64 = this
404                .read_scalar(&this.project_field_named(&place, "AllocationSize")?)?
405                .to_i64()?
406                .try_into()
407                .unwrap();
408            let old_len = match file.file.metadata() {
409                Ok(m) => m.len(),
410                Err(e) => {
411                    this.set_last_error(e)?;
412                    return interp_ok(this.eval_windows("c", "FALSE"));
413                }
414            };
415            if new_alloc_size < old_len {
416                match file.file.set_len(new_alloc_size) {
417                    Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
418                    Err(e) => {
419                        this.set_last_error(e)?;
420                        interp_ok(this.eval_windows("c", "FALSE"))
421                    }
422                }
423            } else {
424                interp_ok(this.eval_windows("c", "TRUE"))
425            }
426        } else {
427            throw_unsup_format!(
428                "SetFileInformationByHandle: Unsupported `FileInformationClass` value {}",
429                class
430            )
431        }
432    }
433
434    fn FlushFileBuffers(
435        &mut self,
436        file: &OpTy<'tcx>, // HANDLE
437    ) -> InterpResult<'tcx, Scalar> {
438        // ^ returns BOOL (i32 on Windows)
439        let this = self.eval_context_mut();
440        this.assert_target_os(Os::Windows, "FlushFileBuffers");
441
442        let file = this.read_handle(file, "FlushFileBuffers")?;
443        let Handle::File(fd_num) = file else { this.invalid_handle("FlushFileBuffers")? };
444        let Some(desc) = this.machine.fds.get(fd_num) else {
445            this.invalid_handle("FlushFileBuffers")?
446        };
447        let file = desc.downcast::<FileHandle>().ok_or_else(|| {
448            err_unsup_format!(
449                "`FlushFileBuffers` is only supported on file-backed file descriptors"
450            )
451        })?;
452
453        if !file.writable {
454            this.set_last_error(IoError::WindowsError("ERROR_ACCESS_DENIED"))?;
455            return interp_ok(this.eval_windows("c", "FALSE"));
456        }
457
458        match file.file.sync_all() {
459            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
460            Err(e) => {
461                this.set_last_error(e)?;
462                interp_ok(this.eval_windows("c", "FALSE"))
463            }
464        }
465    }
466
467    fn MoveFileExW(
468        &mut self,
469        existing_name: &OpTy<'tcx>,
470        new_name: &OpTy<'tcx>,
471        flags: &OpTy<'tcx>,
472    ) -> InterpResult<'tcx, Scalar> {
473        let this = self.eval_context_mut();
474
475        let existing_name = this.read_path_from_wide_str(this.read_pointer(existing_name)?)?;
476        let new_name = this.read_path_from_wide_str(this.read_pointer(new_name)?)?;
477
478        let flags = this.read_scalar(flags)?.to_u32()?;
479
480        // Flag to indicate whether we should replace an existing file.
481        // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
482        let movefile_replace_existing = this.eval_windows_u32("c", "MOVEFILE_REPLACE_EXISTING");
483
484        if flags != movefile_replace_existing {
485            throw_unsup_format!("MoveFileExW: Unsupported `dwFlags` value {}", flags);
486        }
487
488        match std::fs::rename(existing_name, new_name) {
489            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
490            Err(e) => {
491                this.set_last_error(e)?;
492                interp_ok(this.eval_windows("c", "FALSE"))
493            }
494        }
495    }
496
497    fn DeleteFileW(
498        &mut self,
499        file_name: &OpTy<'tcx>, // LPCWSTR
500    ) -> InterpResult<'tcx, Scalar> {
501        // ^ Returns BOOL (i32 on Windows)
502        let this = self.eval_context_mut();
503        this.assert_target_os(Os::Windows, "DeleteFileW");
504        this.check_no_isolation("`DeleteFileW`")?;
505
506        let file_name = this.read_path_from_wide_str(this.read_pointer(file_name)?)?;
507        match std::fs::remove_file(file_name) {
508            Ok(_) => interp_ok(this.eval_windows("c", "TRUE")),
509            Err(e) => {
510                this.set_last_error(e)?;
511                interp_ok(this.eval_windows("c", "FALSE"))
512            }
513        }
514    }
515
516    fn NtWriteFile(
517        &mut self,
518        handle: &OpTy<'tcx>,          // HANDLE
519        event: &OpTy<'tcx>,           // HANDLE
520        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
521        apc_ctx: &OpTy<'tcx>,         // PVOID
522        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
523        buf: &OpTy<'tcx>,             // PVOID
524        n: &OpTy<'tcx>,               // ULONG
525        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
526        key: &OpTy<'tcx>,             // PULONG
527        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
528    ) -> InterpResult<'tcx, ()> {
529        let this = self.eval_context_mut();
530        let handle = this.read_handle(handle, "NtWriteFile")?;
531        let event = this.read_handle(event, "NtWriteFile")?;
532        let apc_routine = this.read_pointer(apc_routine)?;
533        let apc_ctx = this.read_pointer(apc_ctx)?;
534        let buf = this.read_pointer(buf)?;
535        let count = this.read_scalar(n)?.to_u32()?;
536        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
537        let key = this.read_pointer(key)?;
538        let io_status_block =
539            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
540
541        if event != Handle::Null {
542            throw_unsup_format!(
543                "`NtWriteFile` `Event` parameter is non-null, which is unsupported"
544            );
545        }
546
547        if !this.ptr_is_null(apc_routine)? {
548            throw_unsup_format!(
549                "`NtWriteFile` `ApcRoutine` parameter is non-null, which is unsupported"
550            );
551        }
552
553        if !this.ptr_is_null(apc_ctx)? {
554            throw_unsup_format!(
555                "`NtWriteFile` `ApcContext` parameter is non-null, which is unsupported"
556            );
557        }
558
559        if byte_offset != 0 {
560            throw_unsup_format!(
561                "`NtWriteFile` `ByteOffset` parameter is non-null, which is unsupported"
562            );
563        }
564
565        if !this.ptr_is_null(key)? {
566            throw_unsup_format!("`NtWriteFile` `Key` parameter is non-null, which is unsupported");
567        }
568
569        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
570
571        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtWriteFile")? };
572
573        // Windows writes the output code to IO_STATUS_BLOCK.Status, and number of bytes written
574        // to IO_STATUS_BLOCK.Information.
575        // The status block value and the returned value don't need to match - but
576        // for the cases implemented by miri so far, we can choose to decide that they do.
577        let io_status = {
578            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
579            this.project_field_named(&anon, "Status")?
580        };
581        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
582
583        // It seems like short writes are not a thing on Windows, so we don't truncate `count` here.
584        // FIXME: if we are on a Unix host, short host writes are still visible to the program!
585
586        let finish = {
587            let io_status = io_status.clone();
588            let io_status_info = io_status_info.clone();
589            let dest = dest.clone();
590            callback!(
591                @capture<'tcx> {
592                    count: u32,
593                    io_status: MPlaceTy<'tcx>,
594                    io_status_info: MPlaceTy<'tcx>,
595                    dest: MPlaceTy<'tcx>,
596                }
597                |this, result: Result<usize, IoError>| {
598                    match result {
599                        Ok(read_size) => {
600                            assert!(read_size <= count.try_into().unwrap());
601                            // This must fit since `count` fits.
602                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
603                            this.write_int(0, &io_status)?;
604                            this.write_int(0, &dest)
605                        }
606                        Err(e) => {
607                            this.write_int(0, &io_status_info)?;
608                            let status = e.into_ntstatus();
609                            this.write_int(status, &io_status)?;
610                            this.write_int(status, &dest)
611                        }
612                }}
613            )
614        };
615        desc.write(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
616
617        // Return status is written to `dest` and `io_status_block` on callback completion.
618        interp_ok(())
619    }
620
621    fn NtReadFile(
622        &mut self,
623        handle: &OpTy<'tcx>,          // HANDLE
624        event: &OpTy<'tcx>,           // HANDLE
625        apc_routine: &OpTy<'tcx>,     // PIO_APC_ROUTINE
626        apc_ctx: &OpTy<'tcx>,         // PVOID
627        io_status_block: &OpTy<'tcx>, // PIO_STATUS_BLOCK
628        buf: &OpTy<'tcx>,             // PVOID
629        n: &OpTy<'tcx>,               // ULONG
630        byte_offset: &OpTy<'tcx>,     // PLARGE_INTEGER
631        key: &OpTy<'tcx>,             // PULONG
632        dest: &MPlaceTy<'tcx>,        // return type: NTSTATUS
633    ) -> InterpResult<'tcx, ()> {
634        let this = self.eval_context_mut();
635        let handle = this.read_handle(handle, "NtReadFile")?;
636        let event = this.read_handle(event, "NtReadFile")?;
637        let apc_routine = this.read_pointer(apc_routine)?;
638        let apc_ctx = this.read_pointer(apc_ctx)?;
639        let buf = this.read_pointer(buf)?;
640        let count = this.read_scalar(n)?.to_u32()?;
641        let byte_offset = this.read_target_usize(byte_offset)?; // is actually a pointer, but we only support null
642        let key = this.read_pointer(key)?;
643        let io_status_block =
644            this.deref_pointer_as(io_status_block, this.windows_ty_layout("IO_STATUS_BLOCK"))?;
645
646        if event != Handle::Null {
647            throw_unsup_format!("`NtReadFile` `Event` parameter is non-null, which is unsupported");
648        }
649
650        if !this.ptr_is_null(apc_routine)? {
651            throw_unsup_format!(
652                "`NtReadFile` `ApcRoutine` parameter is non-null, which is unsupported"
653            );
654        }
655
656        if !this.ptr_is_null(apc_ctx)? {
657            throw_unsup_format!(
658                "`NtReadFile` `ApcContext` parameter is non-null, which is unsupported"
659            );
660        }
661
662        if byte_offset != 0 {
663            throw_unsup_format!(
664                "`NtReadFile` `ByteOffset` parameter is non-null, which is unsupported"
665            );
666        }
667
668        if !this.ptr_is_null(key)? {
669            throw_unsup_format!("`NtReadFile` `Key` parameter is non-null, which is unsupported");
670        }
671
672        // See NtWriteFile above for commentary on this
673        let io_status = {
674            let anon = this.project_field_named(&io_status_block, "Anonymous")?;
675            this.project_field_named(&anon, "Status")?
676        };
677        let io_status_info = this.project_field_named(&io_status_block, "Information")?;
678
679        let Handle::File(fd) = handle else { this.invalid_handle("NtWriteFile")? };
680
681        let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
682
683        // It seems like short reads are not a thing on Windows, so we don't truncate `count` here.
684        // FIXME: if we are on a Unix host, short host reads are still visible to the program!
685
686        let finish = {
687            let io_status = io_status.clone();
688            let io_status_info = io_status_info.clone();
689            let dest = dest.clone();
690            callback!(
691                @capture<'tcx> {
692                    count: u32,
693                    io_status: MPlaceTy<'tcx>,
694                    io_status_info: MPlaceTy<'tcx>,
695                    dest: MPlaceTy<'tcx>,
696                }
697                |this, result: Result<usize, IoError>| {
698                    match result {
699                        Ok(read_size) => {
700                            assert!(read_size <= count.try_into().unwrap());
701                            // This must fit since `count` fits.
702                            this.write_int(u64::try_from(read_size).unwrap(), &io_status_info)?;
703                            this.write_int(0, &io_status)?;
704                            this.write_int(0, &dest)
705                        }
706                        Err(e) => {
707                            this.write_int(0, &io_status_info)?;
708                            let status = e.into_ntstatus();
709                            this.write_int(status, &io_status)?;
710                            this.write_int(status, &dest)
711                        }
712                }}
713            )
714        };
715        desc.read(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
716
717        // See NtWriteFile for commentary on this
718        interp_ok(())
719    }
720
721    fn SetFilePointerEx(
722        &mut self,
723        file: &OpTy<'tcx>,         // HANDLE
724        dist_to_move: &OpTy<'tcx>, // LARGE_INTEGER
725        new_fp: &OpTy<'tcx>,       // PLARGE_INTEGER
726        move_method: &OpTy<'tcx>,  // DWORD
727    ) -> InterpResult<'tcx, Scalar> {
728        // ^ Returns BOOL (i32 on Windows)
729        let this = self.eval_context_mut();
730        let file = this.read_handle(file, "SetFilePointerEx")?;
731        let dist_to_move = this.read_scalar(dist_to_move)?.to_i64()?;
732        let new_fp_ptr = this.read_pointer(new_fp)?;
733        let move_method = this.read_scalar(move_method)?.to_u32()?;
734
735        let Handle::File(fd) = file else { this.invalid_handle("SetFilePointerEx")? };
736
737        let Some(desc) = this.machine.fds.get(fd) else {
738            throw_unsup_format!("`SetFilePointerEx` is only supported on file backed handles");
739        };
740
741        let file_begin = this.eval_windows_u32("c", "FILE_BEGIN");
742        let file_current = this.eval_windows_u32("c", "FILE_CURRENT");
743        let file_end = this.eval_windows_u32("c", "FILE_END");
744
745        let seek = if move_method == file_begin {
746            SeekFrom::Start(dist_to_move.try_into().unwrap())
747        } else if move_method == file_current {
748            SeekFrom::Current(dist_to_move)
749        } else if move_method == file_end {
750            SeekFrom::End(dist_to_move)
751        } else {
752            throw_unsup_format!("Invalid move method: {move_method}")
753        };
754
755        match desc.seek(this.machine.communicate(), seek)? {
756            Ok(n) => {
757                if !this.ptr_is_null(new_fp_ptr)? {
758                    this.write_scalar(
759                        Scalar::from_i64(n.try_into().unwrap()),
760                        &this.deref_pointer_as(new_fp, this.machine.layouts.i64)?,
761                    )?;
762                }
763                interp_ok(this.eval_windows("c", "TRUE"))
764            }
765            Err(e) => {
766                this.set_last_error(e)?;
767                interp_ok(this.eval_windows("c", "FALSE"))
768            }
769        }
770    }
771}
772
773/// Windows FILETIME is measured in 100-nanosecs since 1601
774fn extract_windows_epoch<'tcx>(
775    ecx: &MiriInterpCx<'tcx>,
776    time: io::Result<SystemTime>,
777) -> InterpResult<'tcx, Option<(u32, u32)>> {
778    match time.ok() {
779        Some(time) => {
780            let duration = ecx.system_time_since_windows_epoch(&time)?;
781            let duration_ticks = ecx.windows_ticks_for(duration)?;
782            #[expect(clippy::as_conversions)]
783            interp_ok(Some((duration_ticks as u32, (duration_ticks >> 32) as u32)))
784        }
785        None => interp_ok(None),
786    }
787}
788
789fn write_filetime_field<'tcx>(
790    cx: &mut MiriInterpCx<'tcx>,
791    val: &MPlaceTy<'tcx>,
792    name: &str,
793    (low, high): (u32, u32),
794) -> InterpResult<'tcx> {
795    cx.write_int_fields_named(
796        &[("dwLowDateTime", low.into()), ("dwHighDateTime", high.into())],
797        &cx.project_field_named(val, name)?,
798    )
799}