openssh_sftp_protocol/
request.rs

1#![forbid(unsafe_code)]
2
3use super::{constants, file_attrs::FileAttrs, open_options::OpenOptions, Handle};
4
5use std::{borrow::Cow, path::Path};
6
7use serde::{Serialize, Serializer};
8use ssh_format::SerOutput;
9
10/// Response with `Response::Version`.
11pub struct Hello {
12    pub version: u32,
13}
14
15impl Serialize for Hello {
16    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
17        (constants::SSH_FXP_INIT, self.version).serialize(serializer)
18    }
19}
20
21#[derive(Debug)]
22pub enum RequestInner<'a> {
23    /// The response to this message will be either
24    /// [`crate::response::ResponseInner::Handle`] (if the operation is successful) or
25    /// [`crate::response::ResponseInner::Status`]
26    /// (if the operation fails).
27    Open(OpenFileRequest<'a>),
28
29    /// Response will be [`crate::response::ResponseInner::Status`].
30    Close(Cow<'a, Handle>),
31
32    /// In response to this request, the server will read as many bytes as it
33    /// can from the file (up to `len'), and return them in a ResponseInner::Data
34    /// message.
35    ///
36    /// If an error occurs or EOF is encountered before reading any
37    /// data, the server will respond with [`crate::response::ResponseInner::Status`].
38    ///
39    /// For normal disk files, it is guaranteed that this will read the specified
40    /// number of bytes, or up to end of file.
41    ///
42    /// For e.g. device files this may return fewer bytes than requested.
43    Read {
44        handle: Cow<'a, Handle>,
45        offset: u64,
46        len: u32,
47    },
48
49    /// Responds with a [`crate::response::ResponseInner::Status`] message.
50    Remove(Cow<'a, Path>),
51
52    /// Responds with a [`crate::response::ResponseInner::Status`] message.
53    Rename {
54        oldpath: Cow<'a, Path>,
55        newpath: Cow<'a, Path>,
56    },
57
58    /// Responds with a [`crate::response::ResponseInner::Status`] message.
59    Mkdir {
60        path: Cow<'a, Path>,
61        attrs: FileAttrs,
62    },
63
64    /// Responds with a [`crate::response::ResponseInner::Status`] message.
65    Rmdir(Cow<'a, Path>),
66
67    /// Responds with a [`crate::response::ResponseInner::Handle`]
68    /// or a [`crate::response::ResponseInner::Status`] message.
69    Opendir(Cow<'a, Path>),
70
71    /// Responds with a [`crate::response::ResponseInner::Name`] or
72    /// a [`crate::response::ResponseInner::Status`] message
73    Readdir(Cow<'a, Handle>),
74
75    /// Responds with [`crate::response::ResponseInner::Attrs`] or
76    /// [`crate::response::ResponseInner::Status`].
77    Stat(Cow<'a, Path>),
78
79    /// Responds with [`crate::response::ResponseInner::Attrs`] or
80    /// [`crate::response::ResponseInner::Status`].
81    ///
82    /// Does not follow symlink.
83    Lstat(Cow<'a, Path>),
84
85    /// Responds with [`crate::response::ResponseInner::Attrs`] or
86    /// [`crate::response::ResponseInner::Status`].
87    Fstat(Cow<'a, Handle>),
88
89    /// Responds with a [`crate::response::ResponseInner::Status`] message.
90    Setstat {
91        path: Cow<'a, Path>,
92        attrs: FileAttrs,
93    },
94
95    /// Responds with a [`crate::response::ResponseInner::Status`] message.
96    Fsetstat {
97        handle: Cow<'a, Handle>,
98        attrs: FileAttrs,
99    },
100
101    /// Responds with [`crate::response::ResponseInner::Name`] with a name and
102    /// dummy attribute value or [`crate::response::ResponseInner::Status`] on error.
103    Readlink(Cow<'a, Path>),
104
105    /// Responds with a [`crate::response::ResponseInner::Status`] message.
106    Symlink {
107        linkpath: Cow<'a, Path>,
108        targetpath: Cow<'a, Path>,
109    },
110
111    /// Responds with [`crate::response::ResponseInner::Name`] with a name and
112    /// dummy attribute value or [`crate::response::ResponseInner::Status`] on error.
113    Realpath(Cow<'a, Path>),
114
115    /// Responds with extended reply, with payload [`crate::response::Limits`].
116    ///
117    /// Extension, only available if it is [`crate::response::Extensions::limits`]
118    /// is returned by [`crate::response::ServerVersion`].
119    Limits,
120
121    /// Same response as [`RequestInner::Realpath`].
122    ///
123    /// Extension, only available if it is [`crate::response::Extensions::expand_path`]
124    /// is returned by [`crate::response::ServerVersion`].
125    ///
126    /// This supports canonicalisation of relative paths and those that need
127    /// tilde-expansion, i.e. "~", "~/..." and "~user/...".
128    ///
129    /// These paths are expanded using shell-lilke rules and the resultant path
130    /// is canonicalised similarly to [`RequestInner::Realpath`].
131    ExpandPath(Cow<'a, Path>),
132
133    /// Same response as [`RequestInner::Setstat`].
134    ///
135    /// Extension, only available if it is [`crate::response::Extensions::lsetstat`]
136    /// is returned by [`crate::response::ServerVersion`].
137    Lsetstat(Cow<'a, Path>, FileAttrs),
138
139    /// Responds with a [`crate::response::ResponseInner::Status`] message.
140    ///
141    /// Extension, only available if it is [`crate::response::Extensions::fsync`]
142    /// is returned by [`crate::response::ServerVersion`].
143    Fsync(Cow<'a, Handle>),
144
145    /// Responds with a [`crate::response::ResponseInner::Status`] message.
146    ///
147    /// Extension, only available if it is [`crate::response::Extensions::hardlink`]
148    /// is returned by [`crate::response::ServerVersion`].
149    HardLink {
150        oldpath: Cow<'a, Path>,
151        newpath: Cow<'a, Path>,
152    },
153
154    /// Responds with a [`crate::response::ResponseInner::Status`] message.
155    ///
156    /// Extension, only available if it is [`crate::response::Extensions::posix_rename`]
157    /// is returned by [`crate::response::ServerVersion`].
158    PosixRename {
159        oldpath: Cow<'a, Path>,
160        newpath: Cow<'a, Path>,
161    },
162
163    /// Responds with a [`crate::response::ResponseInner::Status`] message.
164    ///
165    /// Extension, only available if it is [`crate::response::Extensions::posix_rename`]
166    /// is returned by [`crate::response::ServerVersion`].
167    ///
168    /// For [openssh-portable], this is available from V_9_0_P1.
169    ///
170    /// The server MUST copy the data exactly as if the client had issued a
171    /// series of [`RequestInner::Read`] requests on the `read_from_handle`
172    /// starting at `read_from_offset` and totaling `read_data_length` bytes,
173    /// and issued a series of [`RequestInner::Write`] packets on the
174    /// `write_to_handle`, starting at the `write_from_offset`, and totaling
175    /// the total number of bytes read by the [`RequestInner::Read`] packets.
176    ///
177    /// The server SHOULD allow `read_from_handle` and `write_to_handle` to
178    /// be the same handle as long as the range of data is not overlapping.
179    /// This allows data to efficiently be moved within a file.
180    ///
181    /// If `data_length` is `0`, this imples data should be read until EOF is
182    /// encountered.
183    ///
184    /// There are no protocol restictions on this operation; however, the
185    /// server MUST ensure that the user does not exceed quota, etc.  The
186    /// server is, as always, free to complete this operation out of order if
187    /// it is too large to complete immediately, or to refuse a request that
188    /// is too large.
189    ///
190    /// [openssh-portable]: https://github.com/openssh/openssh-portable
191    Cp {
192        read_from_handle: Cow<'a, Handle>,
193        read_from_offset: u64,
194        read_data_length: u64,
195
196        write_to_handle: Cow<'a, Handle>,
197        write_to_offset: u64,
198    },
199
200    /// The write will extend the file if writing beyond the end of the file.
201    ///
202    /// It is legal to write way beyond the end of the file, the semantics
203    /// are to write zeroes from the end of the file to the specified offset
204    /// and then the data.
205    ///
206    /// On most operating systems, such writes do not allocate disk space but
207    /// instead leave "holes" in the file.
208    ///
209    /// Responds with a [`crate::response::ResponseInner::Status`] message.
210    ///
211    /// The Write also includes any amount of custom data and its size is
212    /// included in the size of the entire packet sent.
213    Write {
214        handle: Cow<'a, Handle>,
215        offset: u64,
216        data: Cow<'a, [u8]>,
217    },
218}
219
220#[derive(Debug)]
221pub struct Request<'a> {
222    pub request_id: u32,
223    pub inner: RequestInner<'a>,
224}
225impl Serialize for Request<'_> {
226    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
227        use RequestInner::*;
228
229        let request_id = self.request_id;
230
231        match &self.inner {
232            Open(params) => (constants::SSH_FXP_OPEN, request_id, params).serialize(serializer),
233            Close(handle) => (constants::SSH_FXP_CLOSE, request_id, handle).serialize(serializer),
234            Read {
235                handle,
236                offset,
237                len,
238            } => (constants::SSH_FXP_READ, request_id, handle, *offset, *len).serialize(serializer),
239
240            Remove(filename) => {
241                (constants::SSH_FXP_REMOVE, request_id, filename).serialize(serializer)
242            }
243
244            Rename { oldpath, newpath } => {
245                (constants::SSH_FXP_RENAME, request_id, oldpath, newpath).serialize(serializer)
246            }
247
248            Mkdir { path, attrs } => {
249                (constants::SSH_FXP_MKDIR, request_id, path, attrs).serialize(serializer)
250            }
251
252            Rmdir(path) => (constants::SSH_FXP_RMDIR, request_id, path).serialize(serializer),
253
254            Opendir(path) => (constants::SSH_FXP_OPENDIR, request_id, path).serialize(serializer),
255
256            Readdir(handle) => {
257                (constants::SSH_FXP_READDIR, request_id, handle).serialize(serializer)
258            }
259
260            Stat(path) => (constants::SSH_FXP_STAT, request_id, path).serialize(serializer),
261
262            Lstat(path) => (constants::SSH_FXP_LSTAT, request_id, path).serialize(serializer),
263
264            Fstat(handle) => (constants::SSH_FXP_FSTAT, request_id, handle).serialize(serializer),
265
266            Setstat { path, attrs } => {
267                (constants::SSH_FXP_SETSTAT, request_id, path, attrs).serialize(serializer)
268            }
269
270            Fsetstat { handle, attrs } => {
271                (constants::SSH_FXP_FSETSTAT, request_id, handle, attrs).serialize(serializer)
272            }
273
274            Readlink(path) => (constants::SSH_FXP_READLINK, request_id, path).serialize(serializer),
275
276            Symlink {
277                linkpath,
278                targetpath,
279            } => {
280                (constants::SSH_FXP_SYMLINK, request_id, targetpath, linkpath).serialize(serializer)
281            }
282
283            Realpath(path) => (constants::SSH_FXP_REALPATH, request_id, path).serialize(serializer),
284
285            Limits => (
286                constants::SSH_FXP_EXTENDED,
287                request_id,
288                constants::EXT_NAME_LIMITS.0,
289            )
290                .serialize(serializer),
291
292            ExpandPath(path) => (
293                constants::SSH_FXP_EXTENDED,
294                request_id,
295                constants::EXT_NAME_EXPAND_PATH.0,
296                path,
297            )
298                .serialize(serializer),
299
300            Lsetstat(path, attrs) => (
301                constants::SSH_FXP_EXTENDED,
302                request_id,
303                constants::EXT_NAME_LSETSTAT.0,
304                path,
305                attrs,
306            )
307                .serialize(serializer),
308
309            Fsync(handle) => (
310                constants::SSH_FXP_EXTENDED,
311                request_id,
312                constants::EXT_NAME_FSYNC.0,
313                handle,
314            )
315                .serialize(serializer),
316
317            HardLink { oldpath, newpath } => (
318                constants::SSH_FXP_EXTENDED,
319                request_id,
320                constants::EXT_NAME_HARDLINK.0,
321                oldpath,
322                newpath,
323            )
324                .serialize(serializer),
325
326            PosixRename { oldpath, newpath } => (
327                constants::SSH_FXP_EXTENDED,
328                request_id,
329                constants::EXT_NAME_POSIX_RENAME.0,
330                oldpath,
331                newpath,
332            )
333                .serialize(serializer),
334
335            Cp {
336                read_from_handle,
337                read_from_offset,
338                read_data_length,
339                write_to_handle,
340                write_to_offset,
341            } => (
342                constants::SSH_FXP_EXTENDED,
343                request_id,
344                constants::EXT_NAME_COPY_DATA.0,
345                read_from_handle,
346                read_from_offset,
347                read_data_length,
348                write_to_handle,
349                write_to_offset,
350            )
351                .serialize(serializer),
352
353            Write {
354                handle,
355                offset,
356                data,
357            } => (constants::SSH_FXP_WRITE, request_id, handle, offset, data).serialize(serializer),
358        }
359    }
360}
361impl Request<'_> {
362    /// The write will extend the file if writing beyond the end of the file.
363    ///
364    /// It is legal to write way beyond the end of the file, the semantics
365    /// are to write zeroes from the end of the file to the specified offset
366    /// and then the data.
367    ///
368    /// On most operating systems, such writes do not allocate disk space but
369    /// instead leave "holes" in the file.
370    ///
371    /// Responds with a [`crate::response::ResponseInner::Status`] message.
372    ///
373    /// The Write also includes any amount of custom data and its size is
374    /// included in the size of the entire packet sent.
375    ///
376    /// Return the serialized header (including the 4-byte size).
377    ///
378    ///  * `serializer` - must be empty
379    pub fn serialize_write_request<Output: SerOutput>(
380        serializer: &mut ssh_format::Serializer<Output>,
381        request_id: u32,
382        handle: Cow<'_, Handle>,
383        offset: u64,
384        data_len: u32,
385    ) -> ssh_format::Result<[u8; 4]> {
386        (
387            constants::SSH_FXP_WRITE,
388            request_id,
389            handle,
390            offset,
391            data_len,
392        )
393            .serialize(&mut *serializer)?;
394
395        serializer.create_header(data_len)
396    }
397}
398
399#[derive(Clone, Debug, Serialize)]
400pub struct OpenFileRequest<'a> {
401    pub(crate) filename: Cow<'a, Path>,
402    pub(crate) flags: u32,
403    pub(crate) attrs: FileAttrs,
404}
405
406impl<'a> OpenFileRequest<'a> {
407    /// Open file in read only mode
408    pub const fn open(filename: Cow<'a, Path>) -> Self {
409        OpenOptions::new().read(true).open(filename)
410    }
411}