From 9563c9e02edd20ee9966ed5c93503a603a6047f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?joseLu=C3=ADs?= Date: Tue, 17 Nov 2020 01:44:29 +0100 Subject: [PATCH] rust: new NcFile type to wrap libc::FILE The notcurses FILE type (`NC_FILE`) is imported via bindgen as a struct, while the equivalent Rust libc::FILE (`LIBC_FILE`) is an opaque enum. Several methods are provided to convert back and forth between both types, so it works both with rust libc operations and notcurses file I/O operations. - new notcurses_debug() test as the first usage of `NcFile` --- rust/src/notcurses.rs | 32 ++-- rust/src/types/file.rs | 339 +++++++++++++++++++++++++++++++++++++++++ rust/src/types/mod.rs | 2 + 3 files changed, 351 insertions(+), 22 deletions(-) create mode 100644 rust/src/types/file.rs diff --git a/rust/src/notcurses.rs b/rust/src/notcurses.rs index 1b537a542..72c5276a0 100644 --- a/rust/src/notcurses.rs +++ b/rust/src/notcurses.rs @@ -232,7 +232,9 @@ pub fn notcurses_term_dim_yx(nc: &Notcurses, rows: &mut i32, cols: &mut i32) { mod test { use serial_test::serial; - use crate::{notcurses_stop, Notcurses}; + use std::io::Read; + + use crate::{notcurses_stop, Notcurses, NcFile}; /* #[test] @@ -260,37 +262,23 @@ mod test { } } - // TODO: a multiplatform way of dealing with C FILE - // - // links: - // https://www.reddit.com/r/rust/comments/8sfjp6/converting_between_file_and_stdfsfile/ - // https://stackoverflow.com/questions/38360996/how-do-i-access-fields-of-a-mut-libcfile #[test] #[serial] - // FIXME: need a solution to deal with _IO_FILE from Rust - #[ignore] fn notcurses_debug() { unsafe { let nc = Notcurses::new(); - - // https://doc.rust-lang.org/stable/std/primitive.pointer.html let mut _p: *mut i8 = &mut 0; let mut _size: *mut usize = &mut 0; - - // https://docs.rs/libc/0.2.80/libc/fn.open_memstream.html - let mut file = libc::open_memstream(&mut _p, _size) - as *mut _ as *mut crate::bindgen::_IO_FILE; - - crate::notcurses_debug(nc, file); + let mut file = NcFile::from_libc(libc::open_memstream(&mut _p, _size)); + crate::notcurses_debug(nc, file.as_nc_ptr()); notcurses_stop(nc); - // as _IO_FILE struct; - let mut debug0 = *file; - println!("{:#?}", debug0); + let mut string1 = String::new(); + let _result = file.read_to_string(&mut string1); + + let string2 = " ************************** notcurses debug state *****************************"; - // as enum libc::FILE - let mut debug1 = file as *mut _ as *mut libc::FILE; - println!("{:#?}", debug1); + assert_eq![&string1[0..string2.len()], &string2[..]]; } } diff --git a/rust/src/types/file.rs b/rust/src/types/file.rs new file mode 100644 index 000000000..61abe8b8b --- /dev/null +++ b/rust/src/types/file.rs @@ -0,0 +1,339 @@ +//! Wrapper for libc::FILE, both as used by notcurses and the libc crate +//! +//! The interface is largely based on the implementation of the +//! [cfile-rs crate](https://github.com/jkarns275/cfile) by Joshua Karns + +use core::ptr::{null_mut, NonNull}; + +use std::{ + // ffi::CString, + io::{Error, ErrorKind, Read, Seek, SeekFrom}, +}; + +pub use libc::{ + c_long, c_void, fclose, feof, fread, fseek, ftell, FILE as LIBC_FILE, SEEK_CUR, SEEK_END, + SEEK_SET, +}; + +pub use crate::bindgen::_IO_FILE as NC_FILE; + +/// Intended to be passed into the CFile::open method. +/// It will open the file in a way that will allow reading and writing, +/// including overwriting old data. +/// It will not create the file if it does not exist. +pub static RANDOM_ACCESS_MODE: &'static str = "rb+"; + +/// Intended to be passed into the CFile::open method. +/// It will open the file in a way that will allow reading and writing, +/// including overwriting old data +pub static UPDATE: &'static str = "rb+"; + +/// Intended to be passed into the CFile::open method. +/// It will only allow reading. +pub static READ_ONLY: &'static str = "r"; + +/// Intended to be passed into the CFile::open method. +/// It will only allow writing. +pub static WRITE_ONLY: &'static str = "w"; + +/// Intended to be passed into the CFile::open method. +/// It will only allow data to be appended to the end of the file. +pub static APPEND_ONLY: &'static str = "a"; + +/// Intended to be passed into the CFile::open method. +/// It will allow data to be appended to the end of the file, and data to be +/// read from the file. It will create the file if it doesn't exist. +pub static APPEND_READ: &'static str = "a+"; + +/// Intended to be passed into the CFile::open method. +/// It will open the file in a way that will allow reading and writing, +/// including overwriting old data. It will create the file if it doesn't exist +pub static TRUNCATE_RANDOM_ACCESS_MODE: &'static str = "wb+"; + +/// A utility function to pull the current value of errno and put it into an +/// Error::Errno +fn get_error() -> Result { + Err(Error::last_os_error()) +} + +/// A wrapper struct around libc::FILE for +/// +/// The notcurses FILE type `NC_FILE` is imported through bindgen as a struct, +/// while the equivalent Rust libc::FILE (`LIBC_FILE`) is an opaque enum. +/// Several methods are provided to convert back and forth between both types, +/// to allow both rust libc operations and notcurses file operations on it. +#[derive(Debug)] +pub struct NcFile { + file_ptr: NonNull, +} + +impl NcFile { + // constructors -- + + /// `NcFile` constructor from a file produced by notcurses + pub fn from_nc(file: *mut NC_FILE) -> Self { + NcFile { + file_ptr: unsafe { NonNull::new_unchecked(NcFile::nc2libc(file)) }, + } + } + + /// `NcFile` constructor from a file produced by the libc crate + pub fn from_libc(file: *mut LIBC_FILE) -> Self { + NcFile { + file_ptr: unsafe { NonNull::new_unchecked(file) }, + } + } + + // methods -- + + /// Returns the file pointer in the format expected by libc + #[inline] + pub fn as_libc_ptr(&self) -> *mut LIBC_FILE { + self.file_ptr.as_ptr() + } + + /// Returns the file pointer in the format expected by notcurses + #[inline] + pub fn as_nc_ptr(&self) -> *mut NC_FILE { + Self::libc2nc(self.file_ptr.as_ptr()) + } + + /// Returns the current position in the file. + /// + /// # Errors + /// On error Error::Errno(errno) is returned. + pub fn current_pos(&self) -> Result { + unsafe { + let pos = ftell(self.as_libc_ptr()); + if pos != -1 { + Ok(pos as u64) + } else { + get_error() + } + } + } + + /// Reads the file from start to end. Convenience method + /// + #[inline] + pub fn read_all(&mut self, buf: &mut Vec) -> Result { + let _ = self.seek(SeekFrom::Start(0)); + self.read_to_end(buf) + } + + // private methods -- + + /// Converts a file pointer from the struct notcurses uses to the + /// opaque enum type libc expects + #[inline] + fn nc2libc(file: *mut NC_FILE) -> *mut LIBC_FILE { + file as *mut _ as *mut LIBC_FILE + } + + /// Converts a file pointer from the libc opaque enum format to the struct + /// expected by notcurses + #[inline] + fn libc2nc(file: *mut LIBC_FILE) -> *mut NC_FILE { + file as *mut _ as *mut NC_FILE + } + + /// A utility function to expand a vector without increasing its capacity + /// more than it needs to be expanded. + fn expand_buffer(buff: &mut Vec, by: usize) { + if buff.capacity() < buff.len() + by { + buff.reserve(by); + } + for _ in 0..by { + buff.push(0u8); + } + } +} + +impl Read for NcFile { + /// Reads exactly the number of bytes required to fill buf. + /// + /// # Errors + /// If the end of the file is reached before buf is filled, + /// Err(EndOfFile(bytes_read)) will be returned. The data that was read + /// before that will still have been placed into buf. + /// + /// Upon some other error, Err(Errno(errno)) will be returned. + fn read(&mut self, buf: &mut [u8]) -> Result { + unsafe { + let result = fread( + buf.as_ptr() as *mut c_void, + 1, + buf.len(), + self.as_libc_ptr(), + ); + if result != buf.len() { + match get_error::() { + Err(err) => { + if err.kind() == ErrorKind::UnexpectedEof { + Ok(result) + } else { + Err(err) + } + } + Ok(_) => panic!("This is impossible"), + } + } else { + Ok(result) + } + } + } + + /// Reads the entire file starting from the current_position + /// expanding buf as needed. + /// + /// On a successful read, this function will return Ok(bytes_read). + /// + /// # Errors + /// If an error occurs during reading, some varient of error will be returned. + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + let pos = self.current_pos(); + let _ = self.seek(SeekFrom::End(0)); + let end = self.current_pos(); + match pos { + Ok(cur_pos) => match end { + Ok(end_pos) => { + if end_pos == cur_pos { + return Ok(0); + } + let to_read = (end_pos - cur_pos) as usize; + if buf.len() < to_read { + let to_reserve = to_read - buf.len(); + Self::expand_buffer(buf, to_reserve); + } + let _ = self.seek(SeekFrom::Start(cur_pos as u64)); + match self.read_exact(buf) { + Ok(()) => Ok(to_read), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + }, + Err(e) => Err(e), + } + } + + /// Reads the entire file from the beginning and stores it in a string + /// + /// On a successful read, this function will return Ok(bytes_read). + /// + /// # Errors + /// If an error occurs during reading, some varient of error will be returned. + fn read_to_string(&mut self, strbuf: &mut String) -> Result { + let mut bytes_read = 0_usize; + let mut buffer = vec![0u8]; + let result = self.read_all(&mut buffer); + + if let Ok(bytes) = result { + bytes_read = bytes; + } + if let Err(e) = result { + return Err(e); + } + + let result = std::str::from_utf8(&buffer); + + if let Ok(strslice) = result { + *strbuf = strslice.to_string(); + Ok(bytes_read) + } else { + get_error() + } + + } + + + /// Reads exactly the number of bytes required to fill buf. + /// + /// # Errors + /// If the end of the file is reached before buf is filled, + /// Err(EndOfFile(bytes_read)) will be returned. The data that was read + /// before that will still have been placed into buf. + /// + /// Upon some other error, Err(Errno(errno)) will be returned. + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { + unsafe { + let result = fread( + buf.as_ptr() as *mut c_void, + 1, + buf.len(), + self.as_libc_ptr(), + ); + if result == buf.len() { + Ok(()) + } else { + // Check if we hit the end of the file + if feof(self.as_libc_ptr()) != 0 { + get_error() + } else { + get_error() + } + } + } + } +} + +impl Seek for NcFile { + /// Changes the current position in the file using the SeekFrom enum. + /// + /// To set relative to the beginning of the file (i.e. index is 0 + offset): + /// ```ignore + /// SeekFrom::Start(offset) + /// ``` + /// To set relative to the end of the file (i.e. index is file_lenth - 1 - offset): + /// ```ignore + /// SeekFrom::End(offset) + /// ``` + /// To set relative to the current position: + /// ```ignore + /// SeekFrom::End(offset) + /// ``` + /// # Errors + /// On error Error::Errno(errno) is returned. + fn seek(&mut self, pos: SeekFrom) -> Result { + unsafe { + let result = match pos { + SeekFrom::Start(from) => fseek(self.as_libc_ptr(), from as c_long, SEEK_SET), + SeekFrom::End(from) => fseek(self.as_libc_ptr(), from as c_long, SEEK_END), + SeekFrom::Current(delta) => fseek(self.as_libc_ptr(), delta as c_long, SEEK_CUR), + }; + if result == 0 { + self.current_pos() + } else { + get_error() + } + } + } +} + +impl Drop for NcFile { + /// Ensures the file stream is closed before abandoning the data. + fn drop(&mut self) { + let _ = unsafe { + if !(self.as_libc_ptr()).is_null() { + let res = fclose(self.as_libc_ptr()); + if res == 0 { + self.file_ptr = NonNull::new_unchecked(null_mut::()); + Ok(()) + } else { + get_error() + } + } else { + Ok(()) + } + }; + } +} + + +// /// A utility function that creates a "buffer" of len bytes. +// // TODO: NcBuffer::new(size) .expand()? +// pub fn buffer(len: usize) -> Vec { +// vec![0u8; len] +// } + + diff --git a/rust/src/types/mod.rs b/rust/src/types/mod.rs index 75e991051..78f30dea3 100644 --- a/rust/src/types/mod.rs +++ b/rust/src/types/mod.rs @@ -15,6 +15,7 @@ mod cell; mod channel; +mod file; mod misc; mod plane; mod terminal; @@ -30,6 +31,7 @@ pub use channel::{ AlphaBits, BlitSet, Channel, Channels, Color, NcFadeCtx, NcPixel, Palette, PaletteIndex, Rgb, CHANNEL_ALPHA_MASK, }; +pub use file::{NcFile, LIBC_FILE, NC_FILE}; pub use misc::{ IntResult, BPREFIXCOLUMNS, BPREFIXSTRLEN, IPREFIXCOLUMNS, IPREFIXSTRLEN, PREFIXCOLUMNS, PREFIXSTRLEN,