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
212
213
214
215
216
217
#![no_std]
#![allow(clippy::result_unit_err)]
#![warn(missing_docs)]

//! This crate allows to access the files of a host machine while semihosting.
//! It should work for all Cortex-M processors.
//! 
//! For examples on how to use this crate, check out the examples on github.

#[macro_use]
extern crate cortex_m_semihosting;

use core::mem;

use cortex_m_semihosting::nr::open;

use cstr_core::CStr;

// FIXME Wait for a working core_io version
// #[cfg(feature="core_io")]
// mod core_io;

/// A reference to an open file on the host system.
///
/// Depending on the options that it was opened with, files can be read and/or written.
///
/// Files are auromatically closed when they are dropped. If an error occurs while dropping, the error is silently swallowed.
/// Use [`close`](File::close) if these errors should be explicitly handled.
///
/// Because all semihosting operations are very slow, buffering reads and writes might be beneficial.
///
/// As the semihosting operations don't return a specific error when they fail, most methods just return a `Result<_, ()>`.
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct File {
    handle: isize,
}

/// This enum determines how a file should be opened.
/// 
/// For more informations on the open modes, see the c-function `fopen`.
#[repr(usize)]
#[allow(missing_docs)]
pub enum FileOpenMode {
    Read = open::R,
    ReadBinary = open::R_BINARY,
    ReadWrite = open::RW,
    ReadWriteAppend = open::RW_APPEND,
    ReadWriteAppendBinary = open::RW_APPEND_BINARY,
    ReadWriteTruncate = open::RW_TRUNC,
    ReadWriteTruncateBinary = open::RW_TRUNC_BINARY,
    WriteAppend = open::W_APPEND,
    WriteAppendBinary = open::W_APPEND_BINARY,
    WriteTruncate = open::W_TRUNC,
    WriteTruncateBinary = open::W_TRUNC_BINARY,
}

/// Specifies the cursor position for [`File::seek`]
pub enum SeekFrom {
    /// Sets the cursor to the specified number of bytes, starting from the beginning of the file
    ///
    /// The provided number must be ranging from 0 to the length of the file
    Start(usize),
    /// Sets the cursor to the length of the file, plus the specified amount.
    ///
    /// Note that the cursor cannot be set to a absolute position greater than the length of the file,
    /// so the relative position provided must be number ranging from the negative length of the file to 0.
    End(isize),
}

/// An Error that occured during seeking
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum SeekError {
    /// The specified position was outside the boundary of the file
    PositionOutOfBounds,
    /// An unknown error occured while reading the length of the file, or setting the cursor.
    Unknown,
}

impl File {
    /// Opens the file with the given mode.
    pub fn open(path: &CStr, mode: FileOpenMode) -> Result<File, ()> {
        let handle = unsafe { syscall!(OPEN, path.as_ptr(), mode, path.to_bytes().len()) } as isize;

        if handle == -1 {
            return Err(());
        }

        Ok(File { handle })
    }

    /// Tries to write all of buffers bytes into the file, and returns the number of bytes that were actually written.
    pub fn write(&mut self, buf: &[u8]) -> Result<usize, ()> {
        let not_written = unsafe { syscall!(WRITE, self.handle, buf.as_ptr(), buf.len()) };

        if not_written > buf.len() {
            return Err(());
        }

        Ok(buf.len() - not_written)
    }

    /// Closes the file.
    ///
    /// The file is also closed when it the file is dropped.
    /// However this method is more explicit and allows to check if an error happenend while closing the file.
    ///
    /// If an error occured, the unclosed file is returned.
    pub fn close(self) -> Result<(), File> {
        match self.close_internal() {
            Ok(()) => {
                // Drop should not be called again,
                // because the file would be closed twice
                mem::forget(self);

                Ok(())
            }
            Err(()) => Err(self),
        }
    }

    fn close_internal(&self) -> Result<(), ()> {
        let success = unsafe { syscall!(CLOSE, self.handle) };

        if success != 0 {
            return Err(());
        }

        Ok(())
    }

    /// Retrieves the total number of bytes of the file
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> Result<usize, ()> {
        let length = unsafe { syscall!(FLEN, self.handle) };

        if (length as isize) < 0 {
            return Err(());
        }

        Ok(length)
    }

    /// Tries to read `buf.len()` bytes from the file and returns the number of bytes that was read into the buffer.
    ///
    /// The result `Ok(0usize)` suggests that EOF has been reached.
    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()> {
        let not_read = unsafe { syscall!(READ, self.handle, buf.as_mut_ptr(), buf.len()) };

        if not_read > buf.len() {
            return Err(());
        }

        Ok(buf.len() - not_read)
    }

    /// Sets the read/write-cursor to the specified position
    /// This actually consists of two semihosting operations:
    /// One to get the length, and another to set the cursor
    /// 
    /// If you want to set the cursor to the beginning of the file, use [`rewind`](File::rewind) instead.
    /// 
    /// see also: [`seek_unchecked`](File::seek_unchecked)
    pub fn seek(&mut self, from: SeekFrom) -> Result<(), SeekError> {
        let length = self.len().map_err(|()| SeekError::Unknown)?;

        let pos = match from {
            SeekFrom::Start(offset) => offset,
            SeekFrom::End(offset) => length.wrapping_add(offset as usize),
        };

        if pos > length {
            return Err(SeekError::PositionOutOfBounds);
        }

        // Safety: pos has been checked, it contains a valid value
        let result = unsafe { self.seek_unchecked(pos) };

        result.map_err(|()| SeekError::Unknown)
    }

    /// Sets the position of the cursor.
    ///
    /// The position is specified relative to the beginning of the file.
    /// 
    /// See also [`seek`](File::seek) for a safe, and slightly more ergonomic way to set the cursor.
    ///
    /// # Safety
    ///
    /// The position must lie inside the extend of the file.
    /// Otherwise, the behaviour is undefined.
    pub unsafe fn seek_unchecked(&mut self, pos: usize) -> Result<(), ()> {
        let result = syscall!(SEEK, self.handle, pos) as isize;

        if result < 0 {
            return Err(());
        }

        Ok(())
    }

    /// Sets the cursor to the beginning of the file.
    /// 
    /// In comparison to [`seek`](File::seek), this method does not need to check the length of the file or the provided index.
    pub fn rewind(&mut self) -> Result<(), ()> {
        // Safety: 0 is always a valid position
        unsafe { self.seek_unchecked(0) }
    }
}

impl Drop for File {
    fn drop(&mut self) {
        // Errors are ignored, just like in std::fs::File
        let result = self.close_internal();
        dbg!(result).unwrap();
        let _ = result;
    }
}