use tauri::Manager as _;
use crate::*;
use super::*;
#[cfg(target_os = "android")]
pub async fn resolve_mime_type<'a, R: tauri::Runtime>(
mime_type: Option<&'a str>,
path: impl AsRef<str>,
app: &tauri::AppHandle<R>,
) -> Result<std::borrow::Cow<'a, str>> {
if let Some(mime_type) = mime_type {
return Ok(mime_type.into())
}
let path = path.as_ref();
let file_name = path.rsplit_once('/')
.map(|(_, file_name)| file_name)
.unwrap_or(path);
if let Some((_, ext)) = file_name.rsplit_once('.') {
let api = app.android_fs_async();
if let Some(mime_type) = api.get_mime_type_from_extension(ext).await? {
return Ok(mime_type.into())
}
}
Ok("application/octet-stream".into())
}
#[cfg(target_os = "android")]
pub async fn resolve_picker_initial_location<R: tauri::Runtime>(
initial_location: PickerInitialLocation,
app: &tauri::AppHandle<R>,
) -> Result<FileUri> {
use std::str::FromStr as _;
let api = app.android_fs_async();
let map_volume_id = |id: Option<&str>| -> Result<Option<StorageVolumeId>> {
match id {
Some(v) => Ok(Some(convert_to_storage_volume_id(v)?)),
None => Ok(None),
}
};
match initial_location {
PickerInitialLocation::Any { uri } => {
if uri.uri.starts_with("content://media") {
#[allow(deprecated)]
if let Ok(path) = api.public_storage().get_path(&uri).await {
if let Ok(volumes) = api.public_storage().get_volumes().await {
for volume in volumes {
let Some(volume_path) = volume.id.top_dir_path.as_ref() else {
continue;
};
let Ok(relative_path) = path.strip_prefix(volume_path) else {
continue;
};
let (base_dir, relative_path) = {
let mut stems = relative_path.components();
let Some(base_dir) = stems.next()
.map(|s| s.as_os_str().to_string_lossy())
.and_then(|s| PublicDir::from_str(&s).ok()) else {
break;
};
stems.next_back();
let relative_path = stems.collect::<std::path::PathBuf>();
(base_dir, relative_path)
};
return api.public_storage().resolve_initial_location(
Some(&volume.id),
base_dir,
relative_path,
true,
).await
}
}
}
}
Ok(uri)
},
PickerInitialLocation::VolumeTop { volume_id } => {
api.resolve_root_initial_location(
map_volume_id(volume_id.as_deref())?.as_ref()
).await
},
PickerInitialLocation::PublicDir { base_dir, relative_path, volume_id } => {
api.public_storage().resolve_initial_location(
map_volume_id(volume_id.as_deref())?.as_ref(),
base_dir,
relative_path.as_deref().unwrap_or(""),
true,
).await
},
}
}
#[cfg(target_os = "android")]
pub fn convert_to_thumbnail_preferred_size(w: f64, h: f64) -> Result<Size> {
if !w.is_finite() || !h.is_finite() {
return Err(Error::with("non-finite width or height"));
}
if w <= 0.0 || h <= 0.0 {
return Err(Error::with(format!("non-positive width or height: ({w}, {h})")));
}
const MAX: u32 = 1000;
let width = u32::clamp(w.round() as u32, 1, MAX);
let height = u32::clamp(h.round() as u32, 1, MAX);
Ok(Size { width, height })
}
#[cfg(target_os = "android")]
pub fn convert_to_image_format(format: &str) -> Result<ImageFormat> {
match format.to_ascii_lowercase().as_str() {
"jpeg" | "jpg" => Ok(ImageFormat::Jpeg),
"webp" => Ok(ImageFormat::Webp),
"png" => Ok(ImageFormat::Png),
_ => Err(Error::with(format!("unexpected image format: {format}")))
}
}
#[cfg(target_os = "android")]
pub fn convert_to_storage_volume_id(id: &str) -> Result<StorageVolumeId> {
serde_json::from_str(id).map_err(Into::into)
}
#[cfg(target_os = "android")]
pub fn convert_from_storage_volume_id(id: &StorageVolumeId) -> Result<String> {
serde_json::to_string(id).map_err(Into::into)
}
#[cfg(target_os = "android")]
pub fn convert_time_to_f64_millis(time: std::time::SystemTime) -> Result<f64> {
let duration = time
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO);
Ok(duration.as_millis() as f64)
}
#[cfg(target_os = "android")]
pub fn convert_bytes_to_base64(bytes: &[u8]) -> Result<String> {
use base64::engine::Engine;
use base64::engine::general_purpose::STANDARD;
Ok(STANDARD.encode(bytes))
}
#[cfg(target_os = "android")]
pub fn convert_bytes_to_data_url(bytes: &[u8], mime_type: &str) -> Result<String> {
use base64::engine::Engine;
use base64::engine::general_purpose::STANDARD;
let mut buffer = format!("data:{mime_type};base64,");
buffer.reserve_exact((bytes.len() * 4 / 3) + 4);
STANDARD.encode_string(bytes, &mut buffer);
Ok(buffer)
}
#[cfg(target_os = "android")]
pub fn convert_base64_to_data_url(base64: &str, mime_type: &str) -> Result<String> {
let mut buffer = format!("data:{mime_type};base64,");
buffer.reserve_exact(base64.len());
buffer.push_str(base64);
Ok(buffer)
}
#[cfg(target_os = "android")]
pub struct CountWriter<W, F> {
writer: W,
on_count: F
}
#[cfg(target_os = "android")]
impl<W, F> CountWriter<W, F> {
pub fn new(writer: W, on_count: F) -> Self {
Self { writer, on_count }
}
}
#[cfg(target_os = "android")]
impl<W: std::io::Write, F: FnMut(usize) -> Result<()>> std::io::Write for CountWriter<W, F> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = self.writer.write(buf)?;
(self.on_count)(n)?;
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub struct ProgressNotificationSettings {
icon: ProgressNotificationIcon,
title: Option<String>,
title_progress: Option<String>,
title_completion: Option<String>,
title_failure: Option<String>,
text: Option<String>,
text_progress: Option<String>,
text_completion: Option<String>,
text_failure: Option<String>,
sub_text: Option<String>,
sub_text_progress: Option<String>,
sub_text_completion: Option<String>,
sub_text_failure: Option<String>,
expected_byte_length: Option<u64>,
force_indeterminate_progress_bar: Option<bool>,
}
#[cfg_attr(not(target_os = "android"), allow(unused))]
impl ProgressNotificationSettings {
pub fn icon(&self) -> ProgressNotificationIcon {
self.icon
}
pub fn expected_byte_length(&self) -> Option<u64> {
self.expected_byte_length
}
pub fn force_indeterminate_progress_bar(&self) -> bool {
self.force_indeterminate_progress_bar.unwrap_or(false)
}
pub fn title_progress(&self) -> Option<&str> {
self.title_progress.as_deref().or(self.title.as_deref())
}
pub fn title_completion(&self) -> Option<&str> {
self.title_completion.as_deref().or(self.title.as_deref())
}
pub fn title_failure(&self) -> Option<&str> {
self.title_failure.as_deref().or(self.title.as_deref())
}
pub fn text_progress(&self) -> Option<&str> {
self.text_progress.as_deref().or(self.text.as_deref())
}
pub fn text_completion(&self) -> Option<&str> {
self.text_completion.as_deref().or(self.text.as_deref())
}
pub fn text_failure(&self) -> Option<&str> {
self.text_failure.as_deref().or(self.text.as_deref())
}
pub fn sub_text_progress(&self) -> Option<&str> {
self.sub_text_progress.as_deref().or(self.sub_text.as_deref())
}
pub fn sub_text_completion(&self) -> Option<&str> {
self.sub_text_completion.as_deref().or(self.sub_text.as_deref())
}
pub fn sub_text_failure(&self) -> Option<&str> {
self.sub_text_failure.as_deref().or(self.sub_text.as_deref())
}
}
#[cfg(target_os = "android")]
const PN_FILE_NAME_PLACEHOLDER: &str = "{{fileName}}";
#[cfg(target_os = "android")]
const PN_PROGRESS_PLACEHOLDER: &str = "{{progress}}";
#[cfg(target_os = "android")]
const PN_PROGRESS_MAX_PLACEHOLDER: &str = "{{progressMax}}";
#[cfg(target_os = "android")]
const PN_PERCENTAGE_PLACEHOLDER: &str = "{{percentage}}";
#[cfg(target_os = "android")]
pub fn has_pn_progress_or_percentage_placeholder(text: Option<&str>) -> bool {
text.is_some_and(|t|
t.contains(PN_PROGRESS_PLACEHOLDER) ||
t.contains(PN_PERCENTAGE_PLACEHOLDER)
)
}
#[cfg(target_os = "android")]
pub fn resolve_pn_placeholders(
text: Option<&str>,
file_name: &str,
progress: Option<u64>,
progress_max: Option<u64>,
) -> Option<String> {
let Some(mut text) = text.map(|s| s.to_string()) else {
return None;
};
if text.contains(PN_FILE_NAME_PLACEHOLDER) {
text = text.replace(PN_FILE_NAME_PLACEHOLDER, file_name);
}
if text.contains(PN_PROGRESS_PLACEHOLDER) {
let progress_str = match progress {
Some(progress) => format_byte_len(progress),
None => "--".to_string()
};
text = text.replace(PN_PROGRESS_PLACEHOLDER, &progress_str);
}
if text.contains(PN_PROGRESS_MAX_PLACEHOLDER) {
let max_str = match progress_max {
Some(progress_max) => format_byte_len(progress_max),
None => "--".to_string()
};
text = text.replace(PN_PROGRESS_MAX_PLACEHOLDER, &max_str);
}
if text.contains(PN_PERCENTAGE_PLACEHOLDER) {
let percentage_str = match Option::zip(progress, progress_max) {
Some((progress, progress_max)) => {
let p = match progress_max {
0 => 100.0,
progress_max => progress as f64 / progress_max as f64 * 100.0
};
(p.clamp(0.0, 100.0) as u64).to_string()
},
None => "--".to_string()
};
text = text.replace(PN_PERCENTAGE_PLACEHOLDER, &percentage_str);
}
Some(text)
}
#[cfg(target_os = "android")]
pub fn format_byte_len(bytes: u64) -> String {
const KB: u64 = 1000;
const MB: u64 = KB * KB;
const GB: u64 = MB * KB;
const TB: u64 = GB * KB;
if bytes < KB {
format!("{} B", bytes)
}
else if bytes < MB {
format!("{:.1} KB", bytes as f64 / KB as f64)
}
else if bytes < GB {
format!("{:.1} MB", bytes as f64 / MB as f64)
}
else if bytes < TB {
format!("{:.1} GB", bytes as f64 / GB as f64)
}
else {
format!("{:.1} TB", bytes as f64 / TB as f64)
}
}
#[cfg(target_os = "android")]
pub struct Throttler {
next_allowed: std::sync::Mutex<std::time::Instant>,
interval: std::time::Duration,
}
#[cfg(target_os = "android")]
impl Throttler {
pub fn new(interval: std::time::Duration) -> Self {
Self {
next_allowed: std::sync::Mutex::new(std::time::Instant::now()),
interval,
}
}
pub fn try_acquire(&self) -> bool {
let mut next_allowed = self.next_allowed.lock().unwrap_or_else(|e| e.into_inner());
let now = std::time::Instant::now();
if now < *next_allowed {
return false
}
*next_allowed = now + self.interval;
true
}
}
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum WriteFileStreamEventInput {
Open {
uri: AfsUriOrFsPath,
supports_raw_ipc_request_body: bool,
options: WriteFileStreamEventInputOptions
},
Write {
id: tauri::ResourceId,
data: Vec<u8>,
},
Close {
id: tauri::ResourceId,
error: bool,
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub struct WriteFileStreamEventInputOptions {
pub append: bool,
pub create: bool,
pub notification: Option<ProgressNotificationSettings>,
}
#[cfg(target_os = "android")]
impl<'a> TryInto<WriteFileStreamEventInput> for tauri::ipc::Request<'a> {
type Error = Error;
fn try_into(self) -> Result<WriteFileStreamEventInput> {
let get_header_value = |header_name: &str| -> Result<std::borrow::Cow<'_, str>> {
self.headers()
.get(header_name)
.ok_or_else(|| Error::missing_value(header_name))
.map(|s| percent_encoding::percent_decode(s.as_ref()))
.and_then(|s| s.decode_utf8().map_err(Into::into))
};
let event_type = get_header_value("eventType")?;
match event_type.as_ref() {
"Open" => {
let supports_raw_ipc_request_body = match self.body() {
tauri::ipc::InvokeBody::Json(_) => false,
tauri::ipc::InvokeBody::Raw(_) => true,
};
let uri = serde_json::from_str(&get_header_value("uri")?)?;
let options = serde_json::from_str(&get_header_value("options")?)?;
Ok(WriteFileStreamEventInput::Open { uri, options, supports_raw_ipc_request_body })
},
"Write" => {
let id = get_header_value("id")?.parse::<u32>()?;
let data = match self.body() {
tauri::ipc::InvokeBody::Raw(body) => {
body.clone()
},
tauri::ipc::InvokeBody::Json(body) => {
let format = body
.get("format")
.ok_or_else(|| Error::missing_value("format"))?
.as_str()
.ok_or_else(|| Error::invalid_type("format"))?;
let data = body
.get("data")
.ok_or_else(|| Error::missing_value("data"))?
.as_str()
.ok_or_else(|| Error::invalid_type("data"))?;
match format {
"dataUrlToDecodedData" => {
let comma_i = data
.find(",")
.ok_or_else(|| Error::with("invalid Data URL"))?;
let (_, b64) = data.split_at(comma_i + 1);
use base64::engine::Engine;
base64::engine::general_purpose::STANDARD.decode(b64)?
},
"textToUtf8" => data.to_string().into_bytes(),
_ => Err(Error::invalid_value("format"))?
}
},
};
Ok(WriteFileStreamEventInput::Write { id, data })
},
"Close" => {
let id = get_header_value("id")?.parse::<u32>()?;
let error = get_header_value("error")?.parse::<bool>()?;
Ok(WriteFileStreamEventInput::Close { id, error })
},
value => Err(Error::invalid_value(value))
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(untagged)]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum WriteFileStreamEventOutput {
Open {
id: tauri::ResourceId,
#[serde(rename="supportsRawIpcRequestBody")]
supports_raw_ipc_request_body: bool
},
Write(()),
Close(()),
}
#[derive(serde::Deserialize)]
#[serde(tag = "type")]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum ReadFileStreamEventInput {
Open {
uri: AfsUriOrFsPath,
},
Read {
id: tauri::ResourceId,
len: u64
},
Close {
id: tauri::ResourceId,
},
}
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum ReadFileStreamEventOutput {
Open(tauri::ResourceId),
Read(Vec<u8>),
Close(()),
}
#[cfg(target_os = "android")]
impl TryFrom<ReadFileStreamEventOutput> for tauri::ipc::Response {
type Error = Error;
fn try_from(v: ReadFileStreamEventOutput) -> Result<tauri::ipc::Response> {
match v {
ReadFileStreamEventOutput::Open(id) => {
let id_bytes = convert_rid_to_bytes(id);
Ok(tauri::ipc::Response::new(id_bytes))
},
ReadFileStreamEventOutput::Read(bytes) => {
Ok(tauri::ipc::Response::new(bytes))
},
ReadFileStreamEventOutput::Close(()) => {
Ok(tauri::ipc::Response::new(Vec::new()))
}
}
}
}
#[cfg(target_os = "android")]
pub struct FileChunkReader {
file: std::fs::File,
read_limit: Option<u64>,
read: u64,
}
#[cfg(target_os = "android")]
impl FileChunkReader {
pub fn new(file: std::fs::File, read_limit: Option<u64>) -> Self {
Self {
read_limit,
read: 0,
file,
}
}
pub fn read_chunk(&mut self, len: u64) -> Result<Vec<u8>> {
use std::io::Read as _;
if self.read_limit.is_some_and(|l| l <= self.read) {
return Ok(Vec::new())
}
let mut nlimit = len;
if let Some(read_limit) = self.read_limit {
nlimit = u64::min(nlimit, read_limit.saturating_sub(self.read));
}
let mut buf = Vec::with_capacity(usize::min(nlimit as usize, 2 * 1024 * 1024));
let nread = self.file
.by_ref()
.take(nlimit)
.read_to_end(&mut buf)?;
self.read += nread as u64;
Ok(buf)
}
}
#[derive(serde::Deserialize)]
#[serde(tag = "type")]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum ReadTextFileLinesStreamEventInput {
Open {
uri: AfsUriOrFsPath,
label: String,
#[serde(rename = "maxLineByteLength")]
max_line_len: u64,
#[serde(rename = "ignoreBOM")]
ignore_bom: bool,
},
Read {
id: tauri::ResourceId,
len: u64,
},
Close {
id: tauri::ResourceId,
},
}
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub enum ReadTextFileLinesStreamEventOutput {
Open(tauri::ResourceId),
Read(Vec<u8>),
Close(()),
}
#[cfg(target_os = "android")]
impl TryFrom<ReadTextFileLinesStreamEventOutput> for tauri::ipc::Response {
type Error = Error;
fn try_from(v: ReadTextFileLinesStreamEventOutput) -> Result<tauri::ipc::Response> {
match v {
ReadTextFileLinesStreamEventOutput::Open(id) => {
let id_bytes = convert_rid_to_bytes(id);
Ok(tauri::ipc::Response::new(id_bytes))
},
ReadTextFileLinesStreamEventOutput::Read(bytes) => {
Ok(tauri::ipc::Response::new(bytes))
},
ReadTextFileLinesStreamEventOutput::Close(()) => {
Ok(tauri::ipc::Response::new(Vec::new()))
}
}
}
}
#[cfg(target_os = "android")]
pub struct FileTextLinesReader {
file: std::io::BufReader<std::fs::File>,
max_line_len: Option<std::num::NonZeroU64>,
line_breaks: LineBreaks,
bom: Option<&'static [u8]>,
bom_handled: bool,
read_limit: Option<u64>,
read: u64
}
#[cfg(target_os = "android")]
impl FileTextLinesReader {
pub fn new(
file: std::fs::File,
max_line_len: Option<std::num::NonZeroU64>,
line_breaks: LineBreaks,
bom: Option<&'static [u8]>,
read_limit: Option<u64>,
) -> Self {
Self {
file: std::io::BufReader::new(file),
max_line_len,
line_breaks,
bom,
read_limit,
bom_handled: false,
read: 0
}
}
pub fn read_lines_framed(&mut self, threshold: u64) -> Result<Vec<u8>> {
use std::io::Read as _;
const ERR_FLAG_LEN: usize = 1;
const LINE_BREAK_TYPE_LEN: usize = 1;
const LINE_LEN_LEN: usize = 8;
const HEADER_LEN: usize = ERR_FLAG_LEN + LINE_BREAK_TYPE_LEN + LINE_LEN_LEN;
const FLAG_OK: u8 = 0;
const FLAG_ERR: u8 = 1;
const LINE_BREAK_NULL: u8 = 0;
const LINE_BREAK_LF: u8 = 1;
const LINE_BREAK_CRLF: u8 = 2;
if self.read_limit.is_some_and(|l| l <= self.read) {
return Ok(Vec::new())
}
let mut buf = Vec::with_capacity(usize::min(threshold as usize, 2 * 1024 * 1024));
loop {
let offset = buf.len();
let header_offset = offset;
let err_flag_offset = header_offset;
let line_break_type_offset = err_flag_offset + ERR_FLAG_LEN;
let line_len_offset = line_break_type_offset + LINE_BREAK_TYPE_LEN;
let line_offset = line_len_offset + LINE_LEN_LEN;
buf.extend_from_slice(&[0; HEADER_LEN]);
let mut nlimit = u64::MAX;
if let Some(read_limit) = self.read_limit {
nlimit = u64::min(nlimit, read_limit.saturating_sub(self.read));
}
if let Some(max_line_len) = self.max_line_len {
let mut alpha = self.line_breaks.lf.len() + self.line_breaks.cr.len();
if !self.bom_handled {
alpha += self.bom.map(|b| b.len()).unwrap_or(0);
}
let max_line_len = max_line_len.get().saturating_add(alpha as u64);
nlimit = u64::min(nlimit, max_line_len);
}
let nread = read_until_bytes(
&mut self.file.by_ref().take(nlimit),
&mut buf,
&self.line_breaks.lf
)?;
self.read += nread as u64;
if nread == 0 || self.read_limit.is_some_and(|l| l <= self.read) {
buf.truncate(offset);
break
}
let mut line_len = nread;
let mut line_break_type = LINE_BREAK_NULL;
if self.line_breaks.lf.len() <= line_len && buf.ends_with(&self.line_breaks.lf) {
buf.truncate(buf.len() - self.line_breaks.lf.len());
line_len -= self.line_breaks.lf.len();
line_break_type = LINE_BREAK_LF;
if self.line_breaks.cr.len() <= line_len && buf.ends_with(&self.line_breaks.cr) {
buf.truncate(buf.len() - self.line_breaks.cr.len());
line_len -= self.line_breaks.cr.len();
line_break_type = LINE_BREAK_CRLF;
}
}
if !self.bom_handled {
self.bom_handled = true;
if let Some(bom) = self.bom {
if buf[line_offset..].starts_with(bom) {
buf.drain(line_offset..line_offset + bom.len());
line_len -= bom.len();
}
}
}
let checked = (|| -> Result<()> {
if self.max_line_len.is_some_and(|i| i.get() < line_len as u64) {
return Err(Error::with("line length limit exceeded"));
}
Ok(())
})();
if let Err(err) = checked {
let err_msg_bytes = err.to_string().into_bytes();
buf[err_flag_offset] = FLAG_ERR;
buf[line_break_type_offset] = LINE_BREAK_NULL;
buf[line_len_offset..(line_len_offset + LINE_LEN_LEN)]
.copy_from_slice(&u64::to_be_bytes(err_msg_bytes.len() as u64));
buf.truncate(line_offset);
buf.extend_from_slice(&err_msg_bytes);
break
}
else {
buf[err_flag_offset] = FLAG_OK;
buf[line_break_type_offset] = line_break_type;
buf[line_len_offset..(line_len_offset + LINE_LEN_LEN)]
.copy_from_slice(&u64::to_be_bytes(line_len as u64));
if threshold <= (buf.len() as u64) {
break
}
}
}
Ok(buf)
}
}
#[cfg(target_os = "android")]
pub fn bom_for_encoding_label(label: &str) -> Option<&'static [u8]> {
match label {
"utf-8" => Some(b"\xEF\xBB\xBF"),
"utf-16le" => Some(b"\xFF\xFE"),
"utf-16be" => Some(b"\xFE\xFF"),
_ => None
}
}
#[cfg(target_os = "android")]
pub struct LineBreaks {
pub lf: &'static [u8],
pub cr: &'static [u8],
}
#[cfg(target_os = "android")]
pub fn line_breaks_for_encoding_label(label: &str) -> LineBreaks {
match label {
"utf-16le" => LineBreaks {
lf: &[0x0A, 0x00],
cr: &[0x0D, 0x00],
},
"utf-16be" => LineBreaks {
lf: &[0x00, 0x0A],
cr: &[0x00, 0x0D],
},
_ => LineBreaks {
lf: &[b'\n'],
cr: &[b'\r'],
},
}
}
#[cfg(target_os = "android")]
fn read_until_bytes(
r: &mut impl std::io::BufRead,
buf: &mut Vec<u8>,
bytes: &[u8]
) -> Result<usize> {
let last_byte = *bytes.last().ok_or_else(|| Error::with("invalid empty bytes"))?;
if bytes.len() == 1 {
return Ok(r.read_until(last_byte, buf)?);
}
let mut total_n = 0;
loop {
let n = r.read_until(last_byte, buf)?;
total_n += n;
if n == 0 || buf.ends_with(bytes) {
return Ok(total_n)
}
}
}
#[cfg(target_os = "android")]
pub fn convert_rid_to_bytes(rid: tauri::ResourceId) -> Vec<u8> {
rid.to_be_bytes().to_vec()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PickerInitialLocation {
Any {
uri: FileUri,
},
VolumeTop {
#[serde(rename = "volumeId")]
volume_id: Option<String>,
},
PublicDir {
#[serde(rename = "baseDir")]
base_dir: PublicDir,
#[serde(rename = "relativePath")]
relative_path: Option<String>,
#[serde(rename = "volumeId")]
volume_id: Option<String>,
},
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Copy)]
pub enum FilePickerType {
FilePicker,
Gallery
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum PublicImageOrGeneralPurposeDir {
Image(PublicImageDir),
GeneralPurpose(PublicGeneralPurposeDir),
}
impl From<PublicImageOrGeneralPurposeDir> for PublicDir {
fn from(value: PublicImageOrGeneralPurposeDir) -> Self {
match value {
PublicImageOrGeneralPurposeDir::Image(d) => d.into(),
PublicImageOrGeneralPurposeDir::GeneralPurpose(d) => d.into(),
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum PublicVideoOrGeneralPurposeDir {
Video(PublicVideoDir),
GeneralPurpose(PublicGeneralPurposeDir),
}
impl From<PublicVideoOrGeneralPurposeDir> for PublicDir {
fn from(value: PublicVideoOrGeneralPurposeDir) -> Self {
match value {
PublicVideoOrGeneralPurposeDir::Video(d) => d.into(),
PublicVideoOrGeneralPurposeDir::GeneralPurpose(d) => d.into(),
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum PublicAudioOrGeneralPurposeDir {
Audio(PublicAudioDir),
GeneralPurpose(PublicGeneralPurposeDir),
}
impl From<PublicAudioOrGeneralPurposeDir> for PublicDir {
fn from(value: PublicAudioOrGeneralPurposeDir) -> Self {
match value {
PublicAudioOrGeneralPurposeDir::Audio(d) => d.into(),
PublicAudioOrGeneralPurposeDir::GeneralPurpose(d) => d.into(),
}
}
}
#[cfg(target_os = "android")]
pub fn validate_path_permission<R: tauri::Runtime>(
path: impl AsRef<std::path::Path>,
app: &tauri::AppHandle<R>,
cmd_scope: &tauri::ipc::CommandScope<AfsScope>,
global_scope: &tauri::ipc::GlobalScope<AfsScope>,
) -> Result<()> {
let path = path.as_ref();
let require_literal_leading_dot = true;
let scope = tauri::scope::fs::Scope::new(
app,
&tauri::utils::config::FsScope::Scope {
allow: global_scope
.allows()
.iter()
.filter_map(|e| e.path.clone())
.chain(cmd_scope.allows().iter().filter_map(|e| e.path.clone()))
.collect(),
deny: global_scope
.denies()
.iter()
.filter_map(|e| e.path.clone())
.chain(cmd_scope.denies().iter().filter_map(|e| e.path.clone()))
.collect(),
require_literal_leading_dot: Some(require_literal_leading_dot),
},
)?;
if !is_forbidden(&scope, &path, require_literal_leading_dot) && scope.is_allowed(&path) {
return Ok(())
}
if cfg!(debug_assertions) {
Err(Error::with(format!(
"forbidden path: {}, maybe it is not allowed on the scope configuration in your capability file",
path.display()
)))
}
else {
Err(Error::with(format!(
"forbidden path: {}",
path.display()
)))
}
}
#[cfg(target_os = "android")]
fn is_forbidden<P: AsRef<std::path::Path>>(
scope: &tauri::fs::Scope,
path: P,
require_literal_leading_dot: bool,
) -> bool {
let path = path.as_ref();
let path = if path.is_symlink() {
match std::fs::read_link(path) {
Ok(p) => p,
Err(_) => return false,
}
} else {
path.to_path_buf()
};
let path = if !path.exists() {
crate::Result::Ok(path)
} else {
std::fs::canonicalize(path).map_err(Into::into)
};
if let Ok(path) = path {
let path: std::path::PathBuf = path.components().collect();
scope.forbidden_patterns().iter().any(|p| {
p.matches_path_with(
&path,
glob::MatchOptions {
require_literal_separator: true,
require_literal_leading_dot,
..Default::default()
},
)
})
} else {
false
}
}
#[derive(Debug)]
#[cfg_attr(not(target_os = "android"), allow(unused))]
pub struct AfsScope {
pub path: Option<std::path::PathBuf>,
}
impl tauri::ipc::ScopeObject for AfsScope {
type Error = Error;
fn deserialize<R: tauri::Runtime>(
app: &tauri::AppHandle<R>,
raw: tauri::utils::acl::Value
) -> Result<Self> {
let path = serde_json::from_value(raw.into()).map(|raw| match raw {
scope::Scope::Value(path) => path,
scope::Scope::Object { path } => path,
})?;
match app.path().parse(path) {
Ok(path) => Ok(Self { path: Some(path) }),
Err(err) => Err(err.into()),
}
}
}