wezterm_ssh/sftp/
types.rs

1use bitflags::bitflags;
2
3bitflags! {
4    struct FileTypeFlags: u32 {
5        const DIR = 0o040000;
6        const FILE = 0o100000;
7        const SYMLINK = 0o120000;
8    }
9}
10
11bitflags! {
12    struct FilePermissionFlags: u32 {
13        const OWNER_READ = 0o400;
14        const OWNER_WRITE = 0o200;
15        const OWNER_EXEC = 0o100;
16        const GROUP_READ = 0o40;
17        const GROUP_WRITE = 0o20;
18        const GROUP_EXEC = 0o10;
19        const OTHER_READ = 0o4;
20        const OTHER_WRITE = 0o2;
21        const OTHER_EXEC = 0o1;
22    }
23}
24
25/// Represents the type associated with a remote file
26#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
27pub enum FileType {
28    Dir,
29    File,
30    Symlink,
31    Other,
32}
33
34impl FileType {
35    /// Returns true if file is a type of directory
36    pub fn is_dir(self) -> bool {
37        matches!(self, Self::Dir)
38    }
39
40    /// Returns true if file is a type of regular file
41    pub fn is_file(self) -> bool {
42        matches!(self, Self::File)
43    }
44
45    /// Returns true if file is a type of symlink
46    pub fn is_symlink(self) -> bool {
47        matches!(self, Self::Symlink)
48    }
49
50    /// Create from a unix mode bitset
51    pub fn from_unix_mode(mode: u32) -> Self {
52        let flags = FileTypeFlags::from_bits_truncate(mode);
53        if flags.contains(FileTypeFlags::DIR) {
54            Self::Dir
55        } else if flags.contains(FileTypeFlags::FILE) {
56            Self::File
57        } else if flags.contains(FileTypeFlags::SYMLINK) {
58            Self::Symlink
59        } else {
60            Self::Other
61        }
62    }
63
64    /// Convert to a unix mode bitset
65    pub fn to_unix_mode(self) -> u32 {
66        let flags = match self {
67            FileType::Dir => FileTypeFlags::DIR,
68            FileType::File => FileTypeFlags::FILE,
69            FileType::Symlink => FileTypeFlags::SYMLINK,
70            FileType::Other => FileTypeFlags::empty(),
71        };
72
73        flags.bits
74    }
75}
76
77/// Represents permissions associated with a remote file
78#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
79pub struct FilePermissions {
80    pub owner_read: bool,
81    pub owner_write: bool,
82    pub owner_exec: bool,
83
84    pub group_read: bool,
85    pub group_write: bool,
86    pub group_exec: bool,
87
88    pub other_read: bool,
89    pub other_write: bool,
90    pub other_exec: bool,
91}
92
93impl FilePermissions {
94    pub fn is_readonly(self) -> bool {
95        !(self.owner_read || self.group_read || self.other_read)
96    }
97
98    /// Create from a unix mode bitset
99    pub fn from_unix_mode(mode: u32) -> Self {
100        let flags = FilePermissionFlags::from_bits_truncate(mode);
101        Self {
102            owner_read: flags.contains(FilePermissionFlags::OWNER_READ),
103            owner_write: flags.contains(FilePermissionFlags::OWNER_WRITE),
104            owner_exec: flags.contains(FilePermissionFlags::OWNER_EXEC),
105            group_read: flags.contains(FilePermissionFlags::GROUP_READ),
106            group_write: flags.contains(FilePermissionFlags::GROUP_WRITE),
107            group_exec: flags.contains(FilePermissionFlags::GROUP_EXEC),
108            other_read: flags.contains(FilePermissionFlags::OTHER_READ),
109            other_write: flags.contains(FilePermissionFlags::OTHER_WRITE),
110            other_exec: flags.contains(FilePermissionFlags::OTHER_EXEC),
111        }
112    }
113
114    /// Convert to a unix mode bitset
115    pub fn to_unix_mode(self) -> u32 {
116        let mut flags = FilePermissionFlags::empty();
117
118        if self.owner_read {
119            flags.insert(FilePermissionFlags::OWNER_READ);
120        }
121        if self.owner_write {
122            flags.insert(FilePermissionFlags::OWNER_WRITE);
123        }
124        if self.owner_exec {
125            flags.insert(FilePermissionFlags::OWNER_EXEC);
126        }
127
128        if self.group_read {
129            flags.insert(FilePermissionFlags::GROUP_READ);
130        }
131        if self.group_write {
132            flags.insert(FilePermissionFlags::GROUP_WRITE);
133        }
134        if self.group_exec {
135            flags.insert(FilePermissionFlags::GROUP_EXEC);
136        }
137
138        if self.other_read {
139            flags.insert(FilePermissionFlags::OTHER_READ);
140        }
141        if self.other_write {
142            flags.insert(FilePermissionFlags::OTHER_WRITE);
143        }
144        if self.other_exec {
145            flags.insert(FilePermissionFlags::OTHER_EXEC);
146        }
147
148        flags.bits
149    }
150}
151
152/// Represents metadata about a remote file
153#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
154pub struct Metadata {
155    /// Type of the remote file
156    pub ty: FileType,
157
158    /// Permissions associated with the file
159    pub permissions: Option<FilePermissions>,
160
161    /// File size, in bytes of the file
162    pub size: Option<u64>,
163
164    /// Owner ID of the file
165    pub uid: Option<u32>,
166
167    /// Owning group of the file
168    pub gid: Option<u32>,
169
170    /// Last access time of the file
171    pub accessed: Option<u64>,
172
173    /// Last modification time of the file
174    pub modified: Option<u64>,
175}
176
177impl Metadata {
178    /// Returns true if metadata is for a directory
179    pub fn is_dir(self) -> bool {
180        self.ty.is_dir()
181    }
182
183    /// Returns true if metadata is for a regular file
184    pub fn is_file(self) -> bool {
185        self.ty.is_file()
186    }
187
188    /// Returns true if metadata is for a symlink
189    pub fn is_symlink(self) -> bool {
190        self.ty.is_symlink()
191    }
192}
193
194/// Represents options to provide when opening a file or directory
195#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
196pub struct OpenOptions {
197    /// If true, opens a file (or directory) for reading
198    pub read: bool,
199
200    /// If provided, opens a file for writing or appending
201    pub write: Option<WriteMode>,
202
203    /// Unix mode that is used when creating a new file
204    pub mode: i32,
205
206    /// Whether opening a file or directory
207    pub ty: OpenFileType,
208}
209
210/// Represents whether opening a file or directory
211#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
212pub enum OpenFileType {
213    Dir,
214    File,
215}
216
217/// Represents different writing modes for opening a file
218#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
219pub enum WriteMode {
220    /// Append data to end of file instead of overwriting it
221    Append,
222
223    /// Overwrite an existing file when opening to write it
224    Write,
225}
226
227/// Represents options to provide when renaming a file or directory
228#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
229pub struct RenameOptions {
230    /// Overwrite the destination if it exists, otherwise fail
231    pub overwrite: bool,
232
233    /// Request atomic rename operation
234    pub atomic: bool,
235
236    /// Request native system calls
237    pub native: bool,
238}
239
240impl Default for RenameOptions {
241    /// Default is to enable all options
242    fn default() -> Self {
243        Self {
244            overwrite: true,
245            atomic: true,
246            native: true,
247        }
248    }
249}
250
251/// Contains libssh2-specific implementations
252#[cfg(feature = "ssh2")]
253mod ssh2_impl {
254    use super::*;
255    use ::ssh2::{
256        FileStat as Ssh2FileStat, FileType as Ssh2FileType, OpenFlags as Ssh2OpenFlags,
257        OpenType as Ssh2OpenType, RenameFlags as Ssh2RenameFlags,
258    };
259
260    impl From<OpenFileType> for Ssh2OpenType {
261        fn from(ty: OpenFileType) -> Self {
262            match ty {
263                OpenFileType::Dir => Self::Dir,
264                OpenFileType::File => Self::File,
265            }
266        }
267    }
268
269    impl From<RenameOptions> for Ssh2RenameFlags {
270        fn from(opts: RenameOptions) -> Self {
271            let mut flags = Self::empty();
272
273            if opts.overwrite {
274                flags |= Self::OVERWRITE;
275            }
276
277            if opts.atomic {
278                flags |= Self::ATOMIC;
279            }
280
281            if opts.native {
282                flags |= Self::NATIVE;
283            }
284
285            flags
286        }
287    }
288
289    impl From<OpenOptions> for Ssh2OpenFlags {
290        fn from(opts: OpenOptions) -> Self {
291            let mut flags = Self::empty();
292
293            if opts.read {
294                flags |= Self::READ;
295            }
296
297            match opts.write {
298                Some(WriteMode::Write) => flags |= Self::WRITE | Self::TRUNCATE,
299                Some(WriteMode::Append) => flags |= Self::WRITE | Self::APPEND | Self::CREATE,
300                None => {}
301            }
302
303            flags
304        }
305    }
306
307    impl From<Ssh2FileType> for FileType {
308        fn from(ft: Ssh2FileType) -> Self {
309            if ft.is_dir() {
310                Self::Dir
311            } else if ft.is_file() {
312                Self::File
313            } else if ft.is_symlink() {
314                Self::Symlink
315            } else {
316                Self::Other
317            }
318        }
319    }
320
321    impl From<Ssh2FileStat> for Metadata {
322        fn from(stat: Ssh2FileStat) -> Self {
323            Self {
324                ty: FileType::from(stat.file_type()),
325                permissions: stat.perm.map(FilePermissions::from_unix_mode),
326                size: stat.size,
327                uid: stat.uid,
328                gid: stat.gid,
329                accessed: stat.atime,
330                modified: stat.mtime,
331            }
332        }
333    }
334
335    impl From<Metadata> for Ssh2FileStat {
336        fn from(metadata: Metadata) -> Self {
337            let ft = metadata.ty;
338
339            Self {
340                perm: metadata
341                    .permissions
342                    .map(|p| p.to_unix_mode() | ft.to_unix_mode()),
343                size: metadata.size,
344                uid: metadata.uid,
345                gid: metadata.gid,
346                atime: metadata.accessed,
347                mtime: metadata.modified,
348            }
349        }
350    }
351}
352
353#[cfg(feature = "libssh-rs")]
354mod libssh_impl {
355    use super::*;
356    use std::time::SystemTime;
357
358    impl From<libssh_rs::FileType> for FileType {
359        fn from(ft: libssh_rs::FileType) -> Self {
360            match ft {
361                libssh_rs::FileType::Directory => Self::Dir,
362                libssh_rs::FileType::Regular => Self::File,
363                libssh_rs::FileType::Symlink => Self::Symlink,
364                _ => Self::Other,
365            }
366        }
367    }
368
369    fn sys_time_to_unix(t: SystemTime) -> u64 {
370        t.duration_since(SystemTime::UNIX_EPOCH)
371            .expect("UNIX_EPOCH < SystemTime")
372            .as_secs()
373    }
374
375    fn unix_to_sys(u: u64) -> SystemTime {
376        SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(u)
377    }
378
379    impl From<libssh_rs::Metadata> for Metadata {
380        fn from(stat: libssh_rs::Metadata) -> Self {
381            Self {
382                ty: stat
383                    .file_type()
384                    .map(FileType::from)
385                    .unwrap_or(FileType::Other),
386                permissions: stat.permissions().map(FilePermissions::from_unix_mode),
387                size: stat.len(),
388                uid: stat.uid(),
389                gid: stat.gid(),
390                accessed: stat.accessed().map(sys_time_to_unix),
391                modified: stat.modified().map(sys_time_to_unix),
392            }
393        }
394    }
395
396    impl Into<libssh_rs::SetAttributes> for Metadata {
397        fn into(self) -> libssh_rs::SetAttributes {
398            let size = self.size;
399            let uid_gid = match (self.uid, self.gid) {
400                (Some(uid), Some(gid)) => Some((uid, gid)),
401                _ => None,
402            };
403            let permissions = self.permissions.map(FilePermissions::to_unix_mode);
404            let atime_mtime = match (self.accessed, self.modified) {
405                (Some(a), Some(m)) => {
406                    let a = unix_to_sys(a);
407                    let m = unix_to_sys(m);
408                    Some((a, m))
409                }
410                _ => None,
411            };
412            libssh_rs::SetAttributes {
413                size,
414                uid_gid,
415                permissions,
416                atime_mtime,
417            }
418        }
419    }
420}