diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3e82e..e44b0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][] - Add `ManageExtension`: Factory reset the entire device or the state of a given client ([#11][]) +- `ChunkedExtension`: Add `AppendFile` and `PartialReadFile` syscalls. [#11]: https://github.com/trussed-dev/trussed-staging/pull/11 diff --git a/src/streaming/mod.rs b/src/streaming/mod.rs index d368b7d..f8277e7 100644 --- a/src/streaming/mod.rs +++ b/src/streaming/mod.rs @@ -4,6 +4,7 @@ mod store; use store::OpenSeekFrom; +#[cfg(feature = "encrypted-chunked")] pub mod utils; #[cfg(feature = "encrypted-chunked")] @@ -85,6 +86,8 @@ pub enum ChunkedRequest { ReadChunk(request::ReadChunk), WriteChunk(request::WriteChunk), AbortChunkedWrite(request::AbortChunkedWrite), + PartialReadFile(request::PartialReadFile), + AppendFile(request::AppendFile), } #[derive(Debug, Deserialize, Serialize)] @@ -99,6 +102,8 @@ pub enum ChunkedReply { StartEncryptedChunkedRead(reply::StartEncryptedChunkedRead), WriteChunk(reply::WriteChunk), AbortChunkedWrite(reply::AbortChunkedWrite), + PartialReadFile(reply::PartialReadFile), + AppendFile(reply::AppendFile), } mod request { @@ -265,6 +270,53 @@ mod request { Self::AbortChunkedWrite(request) } } + + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct PartialReadFile { + pub location: Location, + pub path: PathBuf, + pub offset: usize, + pub length: usize, + } + + impl TryFrom for PartialReadFile { + type Error = Error; + fn try_from(request: ChunkedRequest) -> Result { + match request { + ChunkedRequest::PartialReadFile(request) => Ok(request), + _ => Err(Error::InternalError), + } + } + } + + impl From for ChunkedRequest { + fn from(request: PartialReadFile) -> Self { + Self::PartialReadFile(request) + } + } + + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct AppendFile { + pub location: Location, + pub path: PathBuf, + pub data: Message, + } + + impl TryFrom for AppendFile { + type Error = Error; + fn try_from(request: ChunkedRequest) -> Result { + match request { + ChunkedRequest::AppendFile(request) => Ok(request), + _ => Err(Error::InternalError), + } + } + } + + impl From for ChunkedRequest { + fn from(request: AppendFile) -> Self { + Self::AppendFile(request) + } + } } mod reply { @@ -419,6 +471,49 @@ mod reply { Self::AbortChunkedWrite(reply) } } + + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct PartialReadFile { + pub data: Message, + pub file_length: usize, + } + + impl TryFrom for PartialReadFile { + type Error = Error; + fn try_from(reply: ChunkedReply) -> Result { + match reply { + ChunkedReply::PartialReadFile(reply) => Ok(reply), + _ => Err(Error::InternalError), + } + } + } + + impl From for ChunkedReply { + fn from(reply: PartialReadFile) -> Self { + Self::PartialReadFile(reply) + } + } + + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct AppendFile { + pub file_length: usize, + } + + impl TryFrom for AppendFile { + type Error = Error; + fn try_from(reply: ChunkedReply) -> Result { + match reply { + ChunkedReply::AppendFile(reply) => Ok(reply), + _ => Err(Error::InternalError), + } + } + } + + impl From for ChunkedReply { + fn from(reply: AppendFile) -> Self { + Self::AppendFile(reply) + } + } } impl ExtensionImpl for super::StagingBackend { @@ -502,6 +597,27 @@ impl ExtensionImpl for super::StagingBackend { store::start_chunked_write(store, client_id, &request.path, request.location, &[])?; Ok(reply::StartChunkedWrite {}.into()) } + ChunkedRequest::PartialReadFile(request) => { + let (data, file_length) = store::partial_read_file( + store, + client_id, + &request.path, + request.location, + request.offset, + request.length, + )?; + Ok(reply::PartialReadFile { data, file_length }.into()) + } + ChunkedRequest::AppendFile(request) => { + let file_length = store::append_file( + store, + client_id, + &request.path, + request.location, + &request.data, + )?; + Ok(reply::AppendFile { file_length }.into()) + } #[cfg(feature = "encrypted-chunked")] ChunkedRequest::StartEncryptedChunkedWrite(request) => { clear_chunked_state(store, client_id, backend_ctx)?; @@ -834,6 +950,40 @@ pub trait ChunkedClient: ExtensionClient + FilesystemClient { fn read_file_chunk(&mut self) -> ChunkedResult<'_, reply::ReadChunk, Self> { self.extension(request::ReadChunk {}) } + + /// Partially read a file from a given offset, returning a chunk of the given length and the + /// total file size. + /// + /// If the length is greater than [`trussed::config::MAX_MESSAGE_LENGTH`][] or if the offset is + /// greater than the file size, an error is returned. + fn partial_read_file( + &mut self, + location: Location, + path: PathBuf, + offset: usize, + length: usize, + ) -> ChunkedResult<'_, reply::PartialReadFile, Self> { + self.extension(request::PartialReadFile { + location, + path, + offset, + length, + }) + } + + /// Append data to an existing file and return the size of the file after the write. + fn append_file( + &mut self, + location: Location, + path: PathBuf, + data: Message, + ) -> ChunkedResult<'_, reply::AppendFile, Self> { + self.extension(request::AppendFile { + location, + path, + data, + }) + } } impl + FilesystemClient> ChunkedClient for C {} diff --git a/src/streaming/store.rs b/src/streaming/store.rs index 38d101b..3d57c2b 100644 --- a/src/streaming/store.rs +++ b/src/streaming/store.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 or MIT use littlefs2::driver::Storage as LfsStorage; -use littlefs2::fs::{File, Filesystem}; +use littlefs2::fs::{File, Filesystem, OpenOptions}; use littlefs2::io::{SeekFrom, Write}; use trussed::store::{create_directories, Store}; -use trussed::types::{Bytes, Location, Path, PathBuf}; +use trussed::types::{Bytes, Location, Message, Path, PathBuf}; use trussed::Error; use serde::{Deserialize, Serialize}; @@ -33,9 +33,13 @@ pub fn fs_read_chunk( fs: &Filesystem, path: &Path, pos: OpenSeekFrom, + length: usize, ) -> Result<(Bytes, usize), Error> { let mut contents = Bytes::default(); - contents.resize_default(contents.capacity()).unwrap(); + if length > contents.capacity() { + return Err(Error::FilesystemReadFailure); + } + contents.resize_default(length).unwrap(); let file_len = File::open_and_then(fs, path, |file| { file.seek(pos.into())?; let read_n = file.read(&mut contents)?; @@ -45,6 +49,7 @@ pub fn fs_read_chunk( .map_err(|_| Error::FilesystemReadFailure)?; Ok((contents, file_len)) } + /// Reads contents from path in location of store. #[inline(never)] pub fn read_chunk( @@ -55,9 +60,9 @@ pub fn read_chunk( ) -> Result<(Bytes, usize), Error> { debug_now!("reading chunk {},{:?}", &path, pos); match location { - Location::Internal => fs_read_chunk(store.ifs(), path, pos), - Location::External => fs_read_chunk(store.efs(), path, pos), - Location::Volatile => fs_read_chunk(store.vfs(), path, pos), + Location::Internal => fs_read_chunk(store.ifs(), path, pos, N), + Location::External => fs_read_chunk(store.efs(), path, pos, N), + Location::Volatile => fs_read_chunk(store.vfs(), path, pos, N), } } @@ -290,3 +295,51 @@ pub fn flush_chunks( &client_path, ) } + +pub fn partial_read_file( + store: impl Store, + client_id: &Path, + path: &PathBuf, + location: Location, + offset: usize, + length: usize, +) -> Result<(Message, usize), Error> { + let path = actual_path(client_id, path)?; + let offset = u32::try_from(offset).map_err(|_| Error::FilesystemReadFailure)?; + let pos = OpenSeekFrom::Start(offset); + match location { + Location::Internal => fs_read_chunk(store.ifs(), &path, pos, length), + Location::External => fs_read_chunk(store.efs(), &path, pos, length), + Location::Volatile => fs_read_chunk(store.vfs(), &path, pos, length), + } +} + +fn fs_append_file( + fs: &Filesystem, + path: &Path, + data: &[u8], +) -> Result { + OpenOptions::new() + .write(true) + .append(true) + .open_and_then(fs, path, |file| { + file.write_all(data)?; + file.len() + }) + .map_err(|_| Error::FilesystemWriteFailure) +} + +pub fn append_file( + store: impl Store, + client_id: &Path, + path: &PathBuf, + location: Location, + data: &[u8], +) -> Result { + let path = actual_path(client_id, path)?; + match location { + Location::Internal => fs_append_file(store.ifs(), &path, data), + Location::External => fs_append_file(store.efs(), &path, data), + Location::Volatile => fs_append_file(store.vfs(), &path, data), + } +}