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
5use crate::string;
6use notebook::provider::{Document, Provider};
7use 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.
19pub 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)]
33pub 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
58static 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)]
66pub unsafe extern "C" fn sb_set_provider(table: &Table) {
67 let _ = TABLE.set(*table);
68}
69
70fn 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
79fn 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
96fn 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)]
128pub(crate) struct Host(Mutex<HashMap<i32, u64>>);
129
130impl 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}