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
use crate::{Clircle, Stdio};

use std::convert::TryFrom;
use std::fs::File;
use std::io::{self, Seek, SeekFrom};
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd};
use std::{cmp, hash, ops};

/// Re-export of libc
pub use libc;

/// Implementation of `Clircle` for Unix.
#[derive(Debug)]
pub struct UnixIdentifier {
    device: u64,
    inode: u64,
    size: u64,
    is_regular_file: bool,
    file: Option<File>,
    owns_fd: bool,
}

impl UnixIdentifier {
    fn file(&self) -> &File {
        self.file.as_ref().expect("Called file() on an identifier that has already been destroyed, this should never happen! Please file a bug!")
    }

    fn current_file_offset(&self) -> io::Result<u64> {
        self.file().seek(SeekFrom::Current(0))
    }

    fn has_content_left_to_read(&self) -> io::Result<bool> {
        Ok(self.current_file_offset()? < self.size)
    }

    /// Creates a `UnixIdentifier` from a raw file descriptor. The preferred way to create a
    /// `UnixIdentifier` is through one of the `TryFrom` implementations.
    ///
    /// # Safety
    ///
    /// The `owns_fd` argument should only be true, if the given file descriptor owns the resource
    /// it points to (for example a file).
    /// If it is true, a `File` can be obtained back with `Clircle::into_inner`, or it will be
    /// closed when the `UnixIdentifier` is dropped.
    ///
    /// # Errors
    ///
    /// The underlying call to `File::metadata` fails.
    pub unsafe fn try_from_raw_fd(fd: RawFd, owns_fd: bool) -> io::Result<Self> {
        Self::try_from(File::from_raw_fd(fd)).map(|mut ident| {
            ident.owns_fd = owns_fd;
            ident
        })
    }
}

impl Clircle for UnixIdentifier {
    #[must_use]
    fn into_inner(mut self) -> Option<File> {
        if self.owns_fd {
            self.owns_fd = false;
            self.file.take()
        } else {
            None
        }
    }

    /// This method implements the conflict check that is used in the GNU coreutils program `cat`.
    #[must_use]
    fn surely_conflicts_with(&self, other: &Self) -> bool {
        PartialEq::eq(self, other)
            && self.is_regular_file
            && other.has_content_left_to_read().unwrap_or(true)
    }
}

impl TryFrom<Stdio> for UnixIdentifier {
    type Error = <Self as TryFrom<File>>::Error;

    fn try_from(stdio: Stdio) -> Result<Self, Self::Error> {
        let fd = match stdio {
            Stdio::Stdin => libc::STDIN_FILENO,
            Stdio::Stdout => libc::STDOUT_FILENO,
            Stdio::Stderr => libc::STDERR_FILENO,
        };
        // Safety: It is okay to create the file, because it won't be dropped later since the
        // `owns_fd` field is not set.
        unsafe { Self::try_from_raw_fd(fd, false) }
    }
}

impl ops::Drop for UnixIdentifier {
    fn drop(&mut self) {
        if !self.owns_fd {
            let _ = self.file.take().map(IntoRawFd::into_raw_fd);
        }
    }
}

impl TryFrom<File> for UnixIdentifier {
    type Error = io::Error;

    fn try_from(file: File) -> Result<Self, Self::Error> {
        file.metadata().map(|metadata| Self {
            device: metadata.dev(),
            inode: metadata.ino(),
            size: metadata.size(),
            is_regular_file: metadata.file_type().is_file(),
            file: Some(file),
            owns_fd: true,
        })
    }
}

impl cmp::PartialEq for UnixIdentifier {
    #[must_use]
    fn eq(&self, other: &Self) -> bool {
        self.device == other.device && self.inode == other.inode
    }
}

impl Eq for UnixIdentifier {}

impl hash::Hash for UnixIdentifier {
    fn hash<H: hash::Hasher>(&self, state: &mut H) {
        self.device.hash(state);
        self.inode.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::error::Error;
    use std::io::Write;

    use nix::pty::{openpty, OpenptyResult};
    use nix::unistd::close;

    #[test]
    fn test_fd_closing() -> Result<(), Box<dyn Error>> {
        let dir = tempfile::tempdir().expect("Couldn't create tempdir.");
        let dir_path = dir.path().to_path_buf();

        // 1) Check that the file returned by into_inner is still valid
        let file = File::create(dir_path.join("myfile"))?;
        let ident = UnixIdentifier::try_from(file)?;
        let mut file = ident
            .into_inner()
            .ok_or("Did not get file back from identifier")?;
        // Check if file can be written to without weird errors
        file.write_all(b"Some test content")?;

        // 2) Check that dropping the Identifier does not close the file, if owns_fd is false
        let fd = file.into_raw_fd();
        let ident = unsafe { UnixIdentifier::try_from_raw_fd(fd, false) };
        if let Err(e) = ident {
            let _ = dbg!(close(fd));
            return Err(Box::new(e));
        }
        let ident = ident.unwrap();
        drop(ident);
        close(fd).map_err(|e| {
            format!(
                "Error closing file, that I told UnixIdentifier not to close: {}",
                e
            )
        })?;

        // 3) Check that the file is closed on drop, if owns_fd is true
        let fd = File::open(dir_path.join("myfile"))?.into_raw_fd();
        let ident = unsafe { UnixIdentifier::try_from_raw_fd(fd, true) };
        if let Err(e) = ident {
            let _ = dbg!(close(fd));
            return Err(Box::new(e));
        }
        let ident = ident.unwrap();
        drop(ident);
        close(fd).expect_err("This file descriptor should have been closed already!");

        Ok(())
    }

    #[test]
    fn test_pty_equal_but_not_conflicting() -> Result<(), &'static str> {
        let OpenptyResult { master, slave } = openpty(None, None).expect("Could not open pty.");
        let res = unsafe { UnixIdentifier::try_from_raw_fd(slave, false) }
            .map_err(|_| "Error creating UnixIdentifier from pty fd")
            .and_then(|ident| {
                if !ident.eq(&ident) {
                    return Err("ident != ident");
                }
                if ident.surely_conflicts_with(&ident) {
                    return Err("pty fd does not conflict with itself, but conflict detected");
                }

                let second_ident = unsafe { UnixIdentifier::try_from_raw_fd(slave, false) }
                    .map_err(|_| "Error creating second Identifier to pty")?;
                if !ident.eq(&second_ident) {
                    return Err("ident != second_ident");
                }
                if ident.surely_conflicts_with(&second_ident) {
                    return Err(
                        "Two Identifiers to the same pty should not conflict, but they do.",
                    );
                }
                Ok(())
            });

        let r1 = close(master);
        let r2 = close(slave);

        r1.expect("Error closing master end of pty");
        r2.expect("Error closing slave end of pty");

        res
    }
}