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}