http_serve/
platform.rs

1// Copyright (c) 2016-2021 The http-serve developers
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE.txt or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT.txt or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::convert::TryFrom;
10use std::fs::{File, Metadata};
11use std::io;
12use std::time::SystemTime;
13
14pub trait FileExt {
15    /// Reads at least 1, at most `chunk_size` bytes beginning at `offset`, or fails.
16    ///
17    /// If there are no bytes at `offset`, returns an `UnexpectedEof` error.
18    ///
19    /// The file cursor changes on Windows (like `std::os::windows::fs::seek_read`) but not Unix
20    /// (like `std::os::unix::fs::FileExt::read_at`). The caller never uses the cursor, so this
21    /// doesn't matter.
22    ///
23    /// The implementation goes directly to `libc` or `winapi` to allow soundly reading into an
24    /// uninitialized buffer. This may change after
25    /// [`read_buf`](https://github.com/rust-lang/rust/issues/78485) is stabilized, including buf
26    /// equivalents of `read_at`/`seek_read`.
27    fn read_at(&self, chunk_size: usize, offset: u64) -> io::Result<Vec<u8>>;
28}
29
30impl FileExt for std::fs::File {
31    #[cfg(unix)]
32    fn read_at(&self, chunk_size: usize, offset: u64) -> io::Result<Vec<u8>> {
33        use std::os::unix::io::AsRawFd;
34
35        let mut chunk = Vec::with_capacity(chunk_size);
36        let offset = libc::off_t::try_from(offset).map_err(|_| {
37            std::io::Error::new(std::io::ErrorKind::InvalidInput, "offset too large")
38        })?;
39
40        // SAFETY: `Vec::with_capacity` guaranteed the passed pointers are valid.
41        let retval = unsafe {
42            libc::pread(
43                self.as_raw_fd(),
44                chunk.as_mut_ptr() as *mut libc::c_void,
45                chunk_size,
46                offset,
47            )
48        };
49        let bytes_read = usize::try_from(retval).map_err(|_| std::io::Error::last_os_error())?;
50
51        if bytes_read == 0 {
52            return Err(std::io::Error::new(
53                std::io::ErrorKind::UnexpectedEof,
54                format!("no bytes beyond position {}", offset),
55            ));
56        }
57
58        // SAFETY: `libc::pread` guaranteed these bytes are initialized.
59        unsafe {
60            chunk.set_len(bytes_read);
61        }
62        Ok(chunk)
63    }
64
65    #[cfg(windows)]
66    fn read_at(&self, chunk_size: usize, offset: u64) -> io::Result<Vec<u8>> {
67        // References:
68        // https://github.com/rust-lang/rust/blob/5ffebc2cb3a089c27a4c7da13d09fd2365c288aa/library/std/src/sys/windows/handle.rs#L230
69        // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
70        use std::os::windows::io::AsRawHandle;
71        use winapi::shared::minwindef::DWORD;
72        let handle = self.as_raw_handle();
73        let mut read = 0;
74        let mut chunk = Vec::with_capacity(chunk_size);
75
76        unsafe {
77            // SAFETY: a zero `OVERLAPPED` is valid.
78            let mut overlapped: winapi::um::minwinbase::OVERLAPPED = std::mem::zeroed();
79            overlapped.u.s_mut().Offset = offset as u32;
80            overlapped.u.s_mut().OffsetHigh = (offset >> 32) as u32;
81
82            // SAFETY: `Vec::with_capacity` guaranteed the pointer range is valid.
83            if winapi::um::fileapi::ReadFile(
84                handle,
85                chunk.as_mut_ptr() as *mut winapi::ctypes::c_void,
86                DWORD::try_from(chunk_size).unwrap_or(DWORD::MAX), // saturating conversion
87                &mut read,
88                &mut overlapped,
89            ) == 0
90            {
91                match winapi::um::errhandlingapi::GetLastError() {
92                    #[allow(clippy::print_stderr)]
93                    winapi::shared::winerror::ERROR_IO_PENDING => {
94                        // Match std's <https://github.com/rust-lang/rust/issues/81357> fix:
95                        // abort the process before `overlapped` is dropped.
96                        eprintln!("I/O error: operation failed to complete synchronously");
97                        std::process::abort();
98                    }
99                    winapi::shared::winerror::ERROR_HANDLE_EOF => {
100                        // std::io::Error::from_raw_os_error converts this to ErrorKind::Other.
101                        // Override that.
102                        return Err(std::io::Error::new(
103                            std::io::ErrorKind::UnexpectedEof,
104                            format!("no bytes beyond position {}", offset),
105                        ));
106                    }
107                    o => return Err(std::io::Error::from_raw_os_error(o as i32)),
108                }
109            }
110
111            // SAFETY: `ReadFile` guaranteed these bytes are initialized.
112            chunk.set_len(usize::try_from(read).expect("u32 should fit in usize"));
113        }
114        Ok(chunk)
115    }
116}
117
118pub struct FileInfo {
119    pub inode: u64,
120    pub len: u64,
121    pub mtime: SystemTime,
122}
123
124#[cfg(windows)]
125/// Converts a Windows `FILETIME` to a Rust `SystemTime`
126///
127/// `FILETIME` is the number of 100 ns ticks since Jan 1 1601.
128/// Unix time is the number of seconds since Jan 1 1970.
129fn filetime_to_systemtime(time: winapi::shared::minwindef::FILETIME) -> SystemTime {
130    use std::time::{Duration, UNIX_EPOCH};
131
132    let ticks = (time.dwHighDateTime as u64) << 32 | time.dwLowDateTime as u64;
133
134    // Number of seconds between the Windows and the Unix epoch
135    const SECS_TO_UNIX_EPOCH: u64 = 11_644_473_600;
136    let secs = ticks / 10_000_000 - SECS_TO_UNIX_EPOCH;
137    let nanos = (ticks % 10_000_000 * 100) as u32;
138
139    let duration = Duration::new(secs, nanos);
140    UNIX_EPOCH + duration
141}
142
143#[cfg(unix)]
144pub fn file_info(_file: &File, metadata: &Metadata) -> io::Result<FileInfo> {
145    use std::os::unix::fs::MetadataExt;
146
147    let info = FileInfo {
148        inode: metadata.ino(),
149        len: metadata.len(),
150        mtime: metadata.modified()?,
151    };
152
153    Ok(info)
154}
155
156// TODO: switch to using std::os::windows::fs::MetadataExt when the accessors
157// we need are stable: https://github.com/rust-lang/rust/issues/63010
158// This will reduce the number of system calls and eliminate the winapi crate dependency.
159#[cfg(windows)]
160pub fn file_info(file: &File, _metadata: &Metadata) -> io::Result<FileInfo> {
161    use std::os::windows::io::AsRawHandle;
162    use winapi::shared::minwindef::FILETIME;
163    use winapi::um::fileapi::{self, BY_HANDLE_FILE_INFORMATION};
164
165    let handle = file.as_raw_handle();
166    let zero_time = FILETIME {
167        dwLowDateTime: 0,
168        dwHighDateTime: 0,
169    };
170    let mut info = BY_HANDLE_FILE_INFORMATION {
171        dwFileAttributes: 0,
172        ftCreationTime: zero_time,
173        ftLastAccessTime: zero_time,
174        ftLastWriteTime: zero_time,
175        dwVolumeSerialNumber: 0,
176        nFileSizeHigh: 0,
177        nFileSizeLow: 0,
178        nNumberOfLinks: 0,
179        nFileIndexHigh: 0,
180        nFileIndexLow: 0,
181    };
182
183    let inode = if unsafe { fileapi::GetFileInformationByHandle(handle, &mut info) } != 0 {
184        (info.nFileIndexHigh as u64) << 32 | info.nFileIndexLow as u64
185    } else {
186        return Err(io::Error::last_os_error());
187    };
188    let mtime = filetime_to_systemtime(info.ftLastWriteTime);
189    let len = (info.nFileSizeHigh as u64) << 32 | info.nFileSizeLow as u64;
190
191    let info = FileInfo { inode, len, mtime };
192    Ok(info)
193}