| 1 | //! The host's document provider behind the C surface, as Android's `ContentResolver` reaches |
| 2 | //! documents a picked folder or file grants: a table of functions the host registers once, |
| 3 | //! naming documents by the ids it chooses (their URIs). |
| 4 | |
| 5 | use crate::string; |
| 6 | use notebook::provider::{Document, Provider}; |
| 7 | use std::{ |
| 8 | collections::HashMap, |
| 9 | ffi::{CString, c_char, c_void}, |
| 10 | fs::File, |
| 11 | io, |
| 12 | os::fd::{AsRawFd, FromRawFd}, |
| 13 | sync::{Mutex, OnceLock}, |
| 14 | }; |
| 15 | |
| 16 | /// Hears one document: its id and name, whether it is a folder, its size and last change |
| 17 | /// (milliseconds since 1970) or -1 where the provider gives none, and whether the grant and |
| 18 | /// the provider allow writing it. |
| 19 | pub type Found = extern "C" fn( |
| 20 | context: *mut c_void, |
| 21 | id: *const c_char, |
| 22 | name: *const c_char, |
| 23 | directory: bool, |
| 24 | size: i64, |
| 25 | modified: i64, |
| 26 | writable: bool, |
| 27 | ); |
| 28 | |
| 29 | /// The host's provider, `sb_provider` in `snowbound.h`, which defines its calls' statuses and |
| 30 | /// contract. |
| 31 | #[repr(C)] |
| 32 | #[derive(Clone, Copy)] |
| 33 | pub struct Table { |
| 34 | /// Calls `found` once for the document `id`. |
| 35 | pub document: extern "C" fn(id: *const c_char, found: Found, context: *mut c_void) -> i32, |
| 36 | /// Calls `found` for each document in the folder `id`. |
| 37 | pub children: extern "C" fn(id: *const c_char, found: Found, context: *mut c_void) -> i32, |
| 38 | /// Opens `id` in a `ContentResolver` mode, setting `fd` to a descriptor the |
| 39 | /// caller owns and closes, and `handle` to what `close` finishes. |
| 40 | pub open: extern "C" fn( |
| 41 | id: *const c_char, |
| 42 | mode: *const c_char, |
| 43 | fd: *mut i32, |
| 44 | handle: *mut u64, |
| 45 | ) -> i32, |
| 46 | /// Finishes an open once its descriptor is closed: the provider's reported error, if any |
| 47 | /// (`ParcelFileDescriptor.checkError`), then the host's close. |
| 48 | pub close: extern "C" fn(handle: u64) -> i32, |
| 49 | /// Renames `id` to `name`, calling `renamed` with its new id. |
| 50 | pub rename: extern "C" fn( |
| 51 | id: *const c_char, |
| 52 | name: *const c_char, |
| 53 | renamed: extern "C" fn(*mut c_void, *const c_char), |
| 54 | context: *mut c_void, |
| 55 | ) -> i32, |
| 56 | } |
| 57 | |
| 58 | static TABLE: OnceLock<Table> = OnceLock::new(); |
| 59 | |
| 60 | /// Opens provider notebooks through `table` from now on; set once, before any opens. |
| 61 | /// |
| 62 | /// # Safety |
| 63 | /// Every function in `table` keeps `snowbound.h`'s contract for `sb_provider`: in particular a |
| 64 | /// descriptor `open` hands over is the caller's alone to use and close. |
| 65 | #[unsafe(no_mangle)] |
| 66 | pub unsafe extern "C" fn sb_set_provider(table: &Table) { |
| 67 | let _ = TABLE.set(*table); |
| 68 | } |
| 69 | |
| 70 | fn table() -> io::Result<&'static Table> { |
| 71 | TABLE.get().ok_or_else(|| { |
| 72 | io::Error::new( |
| 73 | io::ErrorKind::Unsupported, |
| 74 | "No document provider is registered", |
| 75 | ) |
| 76 | }) |
| 77 | } |
| 78 | |
| 79 | fn status(code: i32) -> io::Result<()> { |
| 80 | let (kind, message) = match code { |
| 81 | 0 => return Ok(()), |
| 82 | 1 => ( |
| 83 | io::ErrorKind::NotConnected, |
| 84 | "The app keeping the notebook can’t be reached", |
| 85 | ), |
| 86 | 2 => ( |
| 87 | io::ErrorKind::PermissionDenied, |
| 88 | "Snowbound no longer has access to the notebook", |
| 89 | ), |
| 90 | 3 => (io::ErrorKind::NotFound, "The notebook’s file is gone"), |
| 91 | _ => (io::ErrorKind::Other, "The app keeping the notebook failed"), |
| 92 | }; |
| 93 | Err(io::Error::new(kind, message)) |
| 94 | } |
| 95 | |
| 96 | fn described( |
| 97 | call: extern "C" fn(*const c_char, Found, *mut c_void) -> i32, |
| 98 | id: &str, |
| 99 | ) -> io::Result<Vec<Document>> { |
| 100 | extern "C" fn found( |
| 101 | context: *mut c_void, |
| 102 | id: *const c_char, |
| 103 | name: *const c_char, |
| 104 | directory: bool, |
| 105 | size: i64, |
| 106 | modified: i64, |
| 107 | writable: bool, |
| 108 | ) { |
| 109 | // SAFETY: `context` is the list below, alive for the host's call. |
| 110 | let documents = unsafe { &mut *context.cast::<Vec<Document>>() }; |
| 111 | documents.push(Document { |
| 112 | id: string(id), |
| 113 | name: string(name), |
| 114 | directory, |
| 115 | size: u64::try_from(size).ok(), |
| 116 | modified: u64::try_from(modified).ok(), |
| 117 | writable, |
| 118 | }); |
| 119 | } |
| 120 | let id = CString::new(id)?; |
| 121 | let mut documents: Vec<Document> = Vec::new(); |
| 122 | status(call(id.as_ptr(), found, (&raw mut documents).cast()))?; |
| 123 | Ok(documents) |
| 124 | } |
| 125 | |
| 126 | /// The registered table as a `Provider`, with the handle of each descriptor it opened. |
| 127 | #[derive(Default)] |
| 128 | pub(crate) struct Host(Mutex<HashMap<i32, u64>>); |
| 129 | |
| 130 | impl Provider for Host { |
| 131 | fn document(&self, id: &str) -> io::Result<Document> { |
| 132 | described(table()?.document, id)? |
| 133 | .pop() |
| 134 | .ok_or_else(|| io::ErrorKind::NotFound.into()) |
| 135 | } |
| 136 | |
| 137 | fn children(&self, id: &str) -> io::Result<Vec<Document>> { |
| 138 | described(table()?.children, id) |
| 139 | } |
| 140 | |
| 141 | fn open(&self, id: &str, mode: &str) -> io::Result<File> { |
| 142 | let (id, mode) = (CString::new(id)?, CString::new(mode)?); |
| 143 | let (mut fd, mut handle) = (-1, 0); |
| 144 | let table = table()?; |
| 145 | status((table.open)( |
| 146 | id.as_ptr(), |
| 147 | mode.as_ptr(), |
| 148 | &mut fd, |
| 149 | &mut handle, |
| 150 | ))?; |
| 151 | if fd < 0 { |
| 152 | status((table.close)(handle))?; |
| 153 | return Err(io::Error::other("The provider gave no descriptor")); |
| 154 | } |
| 155 | // SAFETY: the host hands over a descriptor only this file uses and closes. |
| 156 | let file = unsafe { File::from_raw_fd(fd) }; |
| 157 | self.0 |
| 158 | .lock() |
| 159 | .unwrap_or_else(|error| error.into_inner()) |
| 160 | .insert(fd, handle); |
| 161 | Ok(file) |
| 162 | } |
| 163 | |
| 164 | /// The descriptor closes first, so a write's end reaches the provider before the host |
| 165 | /// asks for its verdict. |
| 166 | fn close(&self, file: File) -> io::Result<()> { |
| 167 | let handle = self |
| 168 | .0 |
| 169 | .lock() |
| 170 | .unwrap_or_else(|error| error.into_inner()) |
| 171 | .remove(&file.as_raw_fd()); |
| 172 | drop(file); |
| 173 | match handle { |
| 174 | Some(handle) => status((table()?.close)(handle)), |
| 175 | None => Ok(()), |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | fn rename(&self, id: &str, name: &str) -> io::Result<String> { |
| 180 | extern "C" fn renamed(context: *mut c_void, id: *const c_char) { |
| 181 | // SAFETY: `context` is the id below, alive for the host's call. |
| 182 | unsafe { *context.cast::<Option<String>>() = Some(string(id)) }; |
| 183 | } |
| 184 | let (id, name) = (CString::new(id)?, CString::new(name)?); |
| 185 | let mut new: Option<String> = None; |
| 186 | status((table()?.rename)( |
| 187 | id.as_ptr(), |
| 188 | name.as_ptr(), |
| 189 | renamed, |
| 190 | (&raw mut new).cast(), |
| 191 | ))?; |
| 192 | new.ok_or_else(|| io::Error::other("The provider gave no new id")) |
| 193 | } |
| 194 | } |