1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright 2019 Intel Corporation. All Rights Reserved.
//
// Copyright 2018 The Chromium OS Authors. All rights reserved.
//
// SPDX-License-Identifier: (Apache-2.0 AND BSD-3-Clause)

//! Traits and implementations over [lseek64](https://linux.die.net/man/3/lseek64).

use std::fs::File;
use std::io::{Error, Result};
use std::os::unix::io::AsRawFd;

#[cfg(target_env = "musl")]
use libc::{c_int, lseek64, ENXIO};

#[cfg(target_env = "gnu")]
use libc::{lseek64, ENXIO, SEEK_DATA, SEEK_HOLE};

/// A trait for seeking to the next hole or non-hole position in a file.
pub trait SeekHole {
    /// Seek to the first hole in a file.
    ///
    /// Seek at a position greater than or equal to `offset`. If no holes exist
    /// after `offset`, the seek position will be set to the end of the file.
    /// If `offset` is at or after the end of the file, the seek position is
    /// unchanged, and None is returned.
    ///
    /// Returns the current seek position after the seek or an error.
    fn seek_hole(&mut self, offset: u64) -> Result<Option<u64>>;

    /// Seek to the first data in a file.
    ///
    /// Seek at a position greater than or equal to `offset`.
    /// If no data exists after `offset`, the seek position is unchanged,
    /// and None is returned.
    ///
    /// Returns the current offset after the seek or an error.
    fn seek_data(&mut self, offset: u64) -> Result<Option<u64>>;
}

#[cfg(target_env = "musl")]
const SEEK_DATA: c_int = 3;
#[cfg(target_env = "musl")]
const SEEK_HOLE: c_int = 4;

// Safe wrapper for `libc::lseek64()`
fn lseek(file: &mut File, offset: i64, whence: i32) -> Result<Option<u64>> {
    // This is safe because we pass a known-good file descriptor.
    let res = unsafe { lseek64(file.as_raw_fd(), offset, whence) };

    if res < 0 {
        // Convert ENXIO into None; pass any other error as-is.
        let err = Error::last_os_error();
        if let Some(errno) = Error::raw_os_error(&err) {
            if errno == ENXIO {
                return Ok(None);
            }
        }
        Err(err)
    } else {
        Ok(Some(res as u64))
    }
}

impl SeekHole for File {
    fn seek_hole(&mut self, offset: u64) -> Result<Option<u64>> {
        lseek(self, offset as i64, SEEK_HOLE)
    }

    fn seek_data(&mut self, offset: u64) -> Result<Option<u64>> {
        lseek(self, offset as i64, SEEK_DATA)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tempdir::TempDir;
    use std::fs::File;
    use std::io::{Seek, SeekFrom, Write};
    use std::path::PathBuf;

    fn seek_cur(file: &mut File) -> u64 {
        file.seek(SeekFrom::Current(0)).unwrap()
    }

    #[test]
    fn seek_data() {
        let tempdir = TempDir::new_with_prefix("/tmp/seek_data_test").unwrap();
        let mut path = PathBuf::from(tempdir.as_path());
        path.push("test_file");
        let mut file = File::create(&path).unwrap();

        // Empty file
        assert_eq!(file.seek_data(0).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // File with non-zero length consisting entirely of a hole
        file.set_len(0x10000).unwrap();
        assert_eq!(file.seek_data(0).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // seek_data at or after the end of the file should return None
        assert_eq!(file.seek_data(0x10000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);
        assert_eq!(file.seek_data(0x10001).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // Write some data to [0x10000, 0x20000)
        let b = [0x55u8; 0x10000];
        file.seek(SeekFrom::Start(0x10000)).unwrap();
        file.write_all(&b).unwrap();
        assert_eq!(file.seek_data(0).unwrap(), Some(0x10000));
        assert_eq!(seek_cur(&mut file), 0x10000);

        // seek_data within data should return the same offset
        assert_eq!(file.seek_data(0x10000).unwrap(), Some(0x10000));
        assert_eq!(seek_cur(&mut file), 0x10000);
        assert_eq!(file.seek_data(0x10001).unwrap(), Some(0x10001));
        assert_eq!(seek_cur(&mut file), 0x10001);
        assert_eq!(file.seek_data(0x1FFFF).unwrap(), Some(0x1FFFF));
        assert_eq!(seek_cur(&mut file), 0x1FFFF);

        // Extend the file to add another hole after the data
        file.set_len(0x30000).unwrap();
        assert_eq!(file.seek_data(0).unwrap(), Some(0x10000));
        assert_eq!(seek_cur(&mut file), 0x10000);
        assert_eq!(file.seek_data(0x1FFFF).unwrap(), Some(0x1FFFF));
        assert_eq!(seek_cur(&mut file), 0x1FFFF);
        assert_eq!(file.seek_data(0x20000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0x1FFFF);
    }

    #[test]
    #[allow(clippy::cognitive_complexity)]
    fn seek_hole() {
        let tempdir = TempDir::new_with_prefix("/tmp/seek_hole_test").unwrap();
        let mut path = PathBuf::from(tempdir.as_path());
        path.push("test_file");
        let mut file = File::create(&path).unwrap();

        // Empty file
        assert_eq!(file.seek_hole(0).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // File with non-zero length consisting entirely of a hole
        file.set_len(0x10000).unwrap();
        assert_eq!(file.seek_hole(0).unwrap(), Some(0));
        assert_eq!(seek_cur(&mut file), 0);
        assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
        assert_eq!(seek_cur(&mut file), 0xFFFF);

        // seek_hole at or after the end of the file should return None
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x10000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);
        assert_eq!(file.seek_hole(0x10001).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // Write some data to [0x10000, 0x20000)
        let b = [0x55u8; 0x10000];
        file.seek(SeekFrom::Start(0x10000)).unwrap();
        file.write_all(&b).unwrap();

        // seek_hole within a hole should return the same offset
        assert_eq!(file.seek_hole(0).unwrap(), Some(0));
        assert_eq!(seek_cur(&mut file), 0);
        assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
        assert_eq!(seek_cur(&mut file), 0xFFFF);

        // seek_hole within data should return the next hole (EOF)
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x10000).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x10001).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x1FFFF).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);

        // seek_hole at EOF after data should return None
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x20000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // Extend the file to add another hole after the data
        file.set_len(0x30000).unwrap();
        assert_eq!(file.seek_hole(0).unwrap(), Some(0));
        assert_eq!(seek_cur(&mut file), 0);
        assert_eq!(file.seek_hole(0xFFFF).unwrap(), Some(0xFFFF));
        assert_eq!(seek_cur(&mut file), 0xFFFF);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x10000).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x1FFFF).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x20000).unwrap(), Some(0x20000));
        assert_eq!(seek_cur(&mut file), 0x20000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x20001).unwrap(), Some(0x20001));
        assert_eq!(seek_cur(&mut file), 0x20001);

        // seek_hole at EOF after a hole should return None
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x30000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);

        // Write some data to [0x20000, 0x30000)
        file.seek(SeekFrom::Start(0x20000)).unwrap();
        file.write_all(&b).unwrap();

        // seek_hole within [0x20000, 0x30000) should now find the hole at EOF
        assert_eq!(file.seek_hole(0x20000).unwrap(), Some(0x30000));
        assert_eq!(seek_cur(&mut file), 0x30000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x20001).unwrap(), Some(0x30000));
        assert_eq!(seek_cur(&mut file), 0x30000);
        file.seek(SeekFrom::Start(0)).unwrap();
        assert_eq!(file.seek_hole(0x30000).unwrap(), None);
        assert_eq!(seek_cur(&mut file), 0);
    }
}