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
//! A fileserver backed by the RIOT VFS
//!
//! # INCOMPLETE
//!
//! This currently works, but is quite rought around the edges:
//!
//! * Error handling is sloppy, and while most of the file system operations should not fail, a
//!   panic can be forced over the network by requesting a block outside a file's size.
//!
//! * Files do not send ETags along.
//!
//! * Directory listings are in plain text (RFC6690 would need knowledge of the current path), and
//!   do not indicate whether an entry is a directory.

use coap_message::{Code, OptionNumber, ReadableMessage, MinimalWritableMessage, MutableWritableMessage, MessageOption};
use coap_numbers::{option, code};
use coap_handler::Handler;
use coap_message_utils::option_value::Block2RequestData;
use coap_message_utils::OptionsExt;
use coap_handler_implementations::wkc;
use coap_message_utils::Error;
use heapless;

use riot_wrappers::vfs::{File, Dir, SeekFrom};

const MAX_PATH: usize = 64;

struct FileServerRoot(&'static str);

type Path = heapless::String<{ MAX_PATH }>;

// It's kind of pointless to make all the distinctions in the extractor when later sending a Path
// along; a later optimization might look at whether there are any moint points available at
// extraction time and store a static str to the mount point path slice if we can make such an
// assumption.
enum GetSuccess {
    File(File),
    Directory(Path),
}

impl Handler for FileServerRoot {
    type RequestData = (Block2RequestData, GetSuccess);

    type ExtractRequestError = Error;
    type BuildResponseError<M: MinimalWritableMessage> = M::UnionError;

    fn extract_request_data<M: ReadableMessage>(&mut self, req: &M) -> Result<Self::RequestData, Error> {
        use core::fmt::Write;
        let mut path: Result<Path, _> = Ok(self.0.into());

        if req.code().into() != code::GET {
            return Err(Error::method_not_allowed());
        }

        let mut block2 = None;

        req.options()
            .take_block2(&mut block2)
            .take_uri_path(|segment| {
                let err = if let Ok(ref mut path) = &mut path {
                    // FIXME: This is a manual try! block
                    (|| {
                        if segment.contains("/") {
                            // It's not like we have any policy to apply, but let's still not slip this
                            // through.
                            return Err(Error::bad_option(option::URI_PATH));
                        }
                        // possibly with some "out of space / path too long" extra datum
                        path.write_str("/")
                            .map_err(|_| Error::bad_option(option::URI_PATH))?;
                        path.write_str(segment)
                            .map_err(|_| Error::bad_option(option::URI_PATH))?;
                        Ok(())
                    })().err()
                } else {
                    None
                };
                if let Some(err) = err {
                    path = Err(err);
                }
            })
            .ignore_elective_others()?;

        let path = path?;
        if path == "" {
            // Could be nice and tell to append slash...
            return Err(Error::not_found());
        }

        Ok((block2.unwrap_or_default(),
            if path.as_bytes()[path.len() - 1] == b'/' {
                Ok(GetSuccess::Directory(path))
            } else {
                File::open(&path)
                    .map(GetSuccess::File)
            }.map_err(|e| match -e.number() as _ {
                        riot_sys::EACCES => Error::not_found(), // FIXME: forbidden()
                        riot_sys::ENOENT => Error::not_found(),
                        riot_sys::EISDIR => Error::not_found(), // but we could be nice and tell to add a a slash
                        _ => Error::internal_server_error(),
                        })?
            ))
    }

    fn estimate_length(&mut self, _: &Self::RequestData) -> usize {
        // TBD estimate
        1050
    }

    fn build_response<M: MutableWritableMessage>(&mut self, out: &mut M, reqdat: Self::RequestData) -> Result<(), M::UnionError> {
        match reqdat {
            (mut b, GetSuccess::File(mut f)) => {
                out.set_code(Code::new(code::CONTENT)?);

                // 1: payload marker; 5: block option plus intro
                let available_len = out.available_space() - 1;
                b = b.shrink(available_len as _).expect("Buffer can't even keep minimal block");

                // FIXME: What level of error handling of opened files is appropriate?
                let len = f.stat().unwrap().size();

                let more = len > b.start() as usize + b.size() as usize;

                out.add_option_uint(
                    OptionNumber::new(option::BLOCK2)?,
                    b.to_option_value(more)
                )?;

                // FIXME: Set an ETag, possibly from stat data

                f.seek(SeekFrom::Start(b.start() as _)).unwrap(); // FIXME At least *this* should be caught
                let mut payload = out.payload_mut_with_len(b.size() as _)?;
                let mut read_len = 0;
                while !payload.is_empty() {
                    let r = f.read(&mut payload).unwrap();
                    read_len += r;
                    payload = &mut payload[r..];
                    if r == 0 {
                        break;
                    }
                }
                // Not checking whether that came to exactly the seeked length -- if the file
                // length does change between the stat and now, well, we produce an invalid block
                // response.
                out.truncate(read_len)?;
            },
            (b, GetSuccess::Directory(path)) => {
                let dir = Dir::open(&path);
                let mut mountpoints = riot_wrappers::vfs::Mount::all();
                    // .filter(|m| m.mount_point().starts_with(&*path))
                    // .peekable()
                    // ;
                // If mountpoints were an iterator, we could just apply the commented-out stuff
                // above and do `mountpoints.peek().is_some()` here; not having that, doing the
                // iterator stuff above manually (and again below) with possible raciness (which,
                // at worst, makes the headline show but not list any actual mount points)...
                let mut have_some_mountpoints = false;
                while let Some(m) = mountpoints.next() {
                    have_some_mountpoints |= m.mount_point().starts_with(&*path);
                }
                let mut mountpoints = riot_wrappers::vfs::Mount::all();
                // ... and all up to here would be a single line.

                if dir.is_err() && !have_some_mountpoints {
                    out.set_code(Code::new(code::NOT_FOUND)?);
                    return Ok(());
                }

                out.set_code(Code::new(code::CONTENT)?);

                coap_handler_implementations::helpers::block2_write(b, out, |w| {
                    use core::fmt::Write;

                    if let Ok(dir) = dir {
                        writeln!(w, "Index:").unwrap();
                        for e in dir {
                            writeln!(w, "{}", e.name()).unwrap();
                        }
                    }

                    if have_some_mountpoints {
                        writeln!(w, "Relevant mount points:").unwrap();
                        while let Some(m) = mountpoints.next() {
                            // `if` wrapper would be avoided if the filter further up worked
                            if m.mount_point().starts_with(&*path) {
                                writeln!(w, "{}", &m.mount_point()[path.len()..]).unwrap();
                            }
                        }
                    }
                });
            },
        };
        Ok(())
    }
}

/// Build a handler that will serve a subtree of the file system
///
/// Note that this handles a whole subtree, so it's better placed as `.below(&["vfs"],
/// riot_coap_handler_demos::vfs::vfs(""))` (or `"/sda1"`) rather than using `.at()` in a
/// [coap_handler_implementations::HandlerBuilder].
pub fn vfs(root: &'static str) -> impl coap_handler::Handler + coap_handler::Reporting {
    wkc::ConstantSingleRecordReport::new_with_path(
        FileServerRoot(root),
        // Directory listings are currently in plain text
        &[coap_handler::Attribute::Ct(0)],
        // The best we can report right now -- enumerating everything is no good, so gradual reveal
        // it is. If root is not mounted, https://github.com/RIOT-OS/RIOT/issues/15291 strikes.
        &[""],
        )
}