use std::{
collections::{HashMap, HashSet},
io,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use serde_json::Value;
use tokio::sync::Mutex;
use unicode_normalization::UnicodeNormalization;
use super::accessor::{GdriveAccessor, GdriveConfig, MAX_DOCUMENT_BYTES};
use crate::{
BoxFuture,
fs::{Dirent, DirentKind, FileSystem, Stat, filesystem::posix::NAME_MAX},
};
const FOLDER_MIME: &str = "application/vnd.google-apps.folder";
const MY_DRIVE_ID: &str = "root";
const MY_DRIVE_NAME: &str = "My Drive";
const SHARED_WITH_ME_NAME: &str = "Shared with me";
const NATIVE_KINDS: &[(&str, NativeApi, &str)] = &[
(
"application/vnd.google-apps.document",
NativeApi::Doc,
".gdoc.json",
),
(
"application/vnd.google-apps.spreadsheet",
NativeApi::Sheet,
".gsheet.json",
),
(
"application/vnd.google-apps.presentation",
NativeApi::Slides,
".gslide.json",
),
];
fn native_kind(mime: &str) -> Option<(NativeApi, &'static str)> {
NATIVE_KINDS
.iter()
.find(|(m, _, _)| *m == mime)
.map(|(_, api, suffix)| (*api, *suffix))
}
const DIR_TTL: Duration = Duration::from_secs(300);
const GRID_BYTES_BUDGET: u64 = 8 * 1024 * 1024;
const MAX_TABS: usize = 64;
const UNKNOWN_LENGTH_SIZE: u64 = MAX_DOCUMENT_BYTES;
const PAD_LINE: u64 = 4096;
const READ_SPAN: u64 = 64 * 1024 * 1024;
const FIRST_SPAN: u64 = 8 * 1024 * 1024;
const HELD_BUDGET: u64 = READ_SPAN;
const MIN_SPAN: u64 = 4 * 1024 * 1024;
const ACTIVE: Duration = Duration::from_secs(3);
const MAX_REMEMBERED_LENGTHS: usize = 50_000;
const MAX_FOLDER_FILES: usize = 10_000;
const ID_TAG_LEN: usize = 8;
const NAME_BUDGET: usize = NAME_MAX as usize;
fn has_original_bytes(mime: &str) -> bool {
!mime.starts_with("application/vnd.google-apps.")
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Serves {
Nothing,
Original,
Native(NativeApi),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum NativeApi {
Doc,
Sheet,
Slides,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum GKind {
Folder,
SharedDrive,
SharedWithMe,
File,
}
impl GKind {
fn is_dir(self) -> bool {
matches!(
self,
GKind::Folder | GKind::SharedDrive | GKind::SharedWithMe
)
}
}
enum Listing {
Folder {
id: String,
drive_id: Option<String>,
},
SharedWithMe,
}
#[derive(Clone)]
struct Child {
vfs_name: String,
id: String,
drive_id: Option<String>,
kind: GKind,
mtime: Option<std::time::SystemTime>,
created: Option<std::time::SystemTime>,
serves: Serves,
size: Option<u64>,
}
type CachedListing = (Instant, Arc<Vec<Child>>);
type RememberedLength = (Option<std::time::SystemTime>, u64);
struct HeldSpan {
at: u64,
to_eof: bool,
when: Instant,
used: Instant,
bytes: Arc<Vec<u8>>,
}
pub struct GdriveFs {
accessor: GdriveAccessor,
lengths: Mutex<HashMap<String, RememberedLength>>,
dir_cache: Mutex<HashMap<String, CachedListing>>,
held: Mutex<HashMap<String, HeldSpan>>,
}
impl GdriveFs {
pub fn new(config: &GdriveConfig) -> anyhow::Result<Self> {
Ok(Self {
accessor: GdriveAccessor::new(config)?,
lengths: Mutex::new(HashMap::new()),
dir_cache: Mutex::new(HashMap::new()),
held: Mutex::new(HashMap::new()),
})
}
async fn span(&self, id: &str, start: u64, want: u64) -> io::Result<(u64, Arc<Vec<u8>>)> {
let (walking, sharers) = {
let mut held = self.held.lock().await;
let sharers = held
.iter()
.filter(|(k, h)| k.as_str() != id && h.used.elapsed() < ACTIVE)
.count() as u64
+ 1;
let walking = match held.get_mut(id) {
Some(h) if h.when.elapsed() < DIR_TTL => {
if h.at <= start
&& (start.saturating_add(want) <= h.at.saturating_add(h.bytes.len() as u64)
|| h.to_eof)
{
h.used = Instant::now();
return Ok((h.at, h.bytes.clone()));
}
h.at <= start && start <= h.at.saturating_add(h.bytes.len() as u64)
}
_ => false,
};
(walking, sharers)
};
let share = (HELD_BUDGET / sharers).max(MIN_SPAN);
let len = want.max(if walking {
share
} else {
share.min(FIRST_SPAN)
});
let bytes = self
.accessor
.download(id, Some(start..start.saturating_add(len)))
.await
.map_err(not_found_or_backend)?;
let to_eof = (bytes.len() as u64) < len;
let bytes = Arc::new(bytes);
let now = Instant::now();
self.hold(
id,
HeldSpan {
at: start,
to_eof,
when: now,
used: now,
bytes: bytes.clone(),
},
)
.await;
Ok((start, bytes))
}
async fn hold(&self, id: &str, span: HeldSpan) {
let len = span.bytes.len() as u64;
let mut held = self.held.lock().await;
held.remove(id);
held.retain(|_, h| h.when.elapsed() < DIR_TTL);
while !held.is_empty()
&& held.values().map(|h| h.bytes.len() as u64).sum::<u64>() + len > HELD_BUDGET
{
let coldest = held
.iter()
.min_by_key(|(_, h)| h.used)
.map(|(k, _)| k.clone());
match coldest {
Some(k) => {
held.remove(&k);
}
None => break,
}
}
held.insert(id.to_string(), span);
}
async fn rendered_json(&self, child: &Child, api: NativeApi) -> io::Result<Arc<Vec<u8>>> {
let id = child.id.as_str();
{
let mut held = self.held.lock().await;
if let Some(h) = held.get_mut(id)
&& h.at == 0
&& h.to_eof
&& h.when.elapsed() < DIR_TTL
{
h.used = Instant::now();
return Ok(h.bytes.clone());
}
}
let bytes = match api {
NativeApi::Sheet => self.spreadsheet_bytes(id).await,
NativeApi::Doc => self.accessor.document_json(id).await,
NativeApi::Slides => self.accessor.presentation_json(id).await,
}
.map_err(not_found_or_backend)?;
let bytes = Arc::new(bytes);
let now = Instant::now();
self.hold(
id,
HeldSpan {
at: 0,
to_eof: true,
when: now,
used: now,
bytes: bytes.clone(),
},
)
.await;
self.remember_len(child, bytes.len() as u64).await;
Ok(bytes)
}
async fn root_sections(&self) -> (Vec<Child>, bool) {
let section = |name: &str, id: &str, kind: GKind, drive_id: Option<String>| Child {
vfs_name: name.to_string(),
id: id.to_string(),
drive_id,
kind,
mtime: None,
created: None,
serves: Serves::Nothing,
size: None,
};
let mut children = vec![
section(MY_DRIVE_NAME, MY_DRIVE_ID, GKind::Folder, None),
section(SHARED_WITH_ME_NAME, "", GKind::SharedWithMe, None),
];
let listed = self.accessor.list_shared_drives().await;
if let Err(e) = &listed {
eprintln!("gdrive: shared drives could not be listed, root omits them: {e:#}");
}
let complete = listed.is_ok();
if let Ok(mut drives) = listed {
drives.sort_by(|a, b| {
let key = |d: &Value| {
let name: String = d
.get("name")
.and_then(|x| x.as_str())
.unwrap_or("")
.nfc()
.collect();
let id = d
.get("id")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string();
(name, id)
};
key(a).cmp(&key(b))
});
let mut existing: HashSet<String> = children
.iter()
.map(|c| c.vfs_name.nfc().collect())
.collect();
for d in &drives {
if let (Some(id), Some(name)) = (
d.get("id").and_then(|x| x.as_str()),
d.get("name").and_then(|x| x.as_str()),
) {
let vfs_name = unique_name(&sanitize_name(name), &existing);
existing.insert(vfs_name.nfc().collect());
children.push(section(
&vfs_name,
id,
GKind::SharedDrive,
Some(id.to_string()),
));
}
}
}
(children, complete)
}
async fn list_dir(&self, folder: &str) -> io::Result<Arc<Vec<Child>>> {
{
let cache = self.dir_cache.lock().await;
if let Some((at, children)) = cache.get(folder)
&& at.elapsed() < DIR_TTL
{
return Ok(children.clone());
}
}
let mut complete = true;
let mut children = if folder == "/" {
let (sections, ok) = self.root_sections().await;
complete = ok;
sections
} else {
let listing = self.how_to_list(folder).await?;
let (files, drive_id) = match &listing {
Listing::SharedWithMe => (
self.accessor.list_shared_with_me(MAX_FOLDER_FILES).await,
None,
),
Listing::Folder { id, drive_id } => (
self.accessor
.list_files(id, drive_id.as_deref(), MAX_FOLDER_FILES)
.await,
drive_id.clone(),
),
};
let files = files.map_err(not_found_or_backend)?;
let mut children: Vec<Child> = files.iter().filter_map(child_from_file).collect();
if let Some(d) = &drive_id {
for c in children.iter_mut() {
c.drive_id.get_or_insert_with(|| d.clone());
}
}
children
};
if folder != "/" {
disambiguate(&mut children);
}
let children = Arc::new(children);
if complete {
let mut cache = self.dir_cache.lock().await;
cache.retain(|_, (at, _)| at.elapsed() < DIR_TTL);
cache.insert(folder.to_string(), (Instant::now(), children.clone()));
}
Ok(children)
}
async fn how_to_list(&self, folder: &str) -> io::Result<Listing> {
let (parent, name) = split_last(folder);
let children = Box::pin(self.list_dir(&parent)).await?;
let entry = children
.iter()
.find(|c| same_name(&c.vfs_name, &name) && c.kind.is_dir())
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
Ok(match entry.kind {
GKind::SharedWithMe => Listing::SharedWithMe,
_ => Listing::Folder {
id: entry.id.clone(),
drive_id: entry.drive_id.clone(),
},
})
}
async fn spreadsheet_bytes(&self, id: &str) -> anyhow::Result<Vec<u8>> {
let mut v: Value = serde_json::from_slice(&self.accessor.spreadsheet_json(id).await?)?;
let titles: Vec<String> = tab_titles(&v);
if titles.is_empty() {
return pretty(&v);
}
let batch = match self.accessor.sheet_values_batch(id, &titles).await {
Ok(b) => b,
Err(e) => {
if let Some(obj) = v.as_object_mut() {
obj.insert("valuesUnavailable".into(), Value::String(format!("{e:#}")));
}
return pretty(&v);
}
};
fold_values(&mut v, &batch, &titles);
pretty(&v)
}
async fn remembered_len(&self, child: &Child) -> Option<u64> {
let in_hand = {
let held = self.held.lock().await;
held.get(child.id.as_str())
.filter(|h| h.at == 0 && h.to_eof)
.filter(|h| h.when.elapsed() < DIR_TTL)
.map(|h| h.bytes.len() as u64)
};
if in_hand.is_some() {
return in_hand;
}
self.lengths
.lock()
.await
.get(&child.id)
.filter(|(mtime, _)| *mtime == child.mtime)
.map(|(_, len)| *len)
}
async fn remember_len(&self, child: &Child, len: u64) {
if child.mtime.is_none() {
return;
}
let mut lengths = self.lengths.lock().await;
if lengths.len() >= MAX_REMEMBERED_LENGTHS {
lengths.clear();
}
lengths.insert(child.id.clone(), (child.mtime, len));
}
#[cfg(test)]
pub(crate) async fn listings_retained(&self) -> usize {
self.dir_cache.lock().await.len()
}
#[cfg(test)]
pub(crate) async fn held_bytes(&self, id: &str) -> Option<u64> {
self.held.lock().await.get(id).map(|h| h.bytes.len() as u64)
}
#[cfg(test)]
pub(crate) async fn forget_rendered_for_test(&self) {
self.held.lock().await.clear();
}
#[cfg(test)]
pub(crate) async fn age_spans_for_test(&self, by: Duration) {
let mut held = self.held.lock().await;
for h in held.values_mut() {
if let (Some(w), Some(u)) = (h.when.checked_sub(by), h.used.checked_sub(by)) {
h.when = w;
h.used = u;
}
}
}
#[cfg(test)]
pub(crate) async fn lengths_remembered(&self) -> usize {
self.lengths.lock().await.len()
}
#[cfg(test)]
pub(crate) async fn age_listings_for_test(&self) {
let mut cache = self.dir_cache.lock().await;
let stale = Instant::now() - DIR_TTL - Duration::from_secs(1);
for (at, _) in cache.values_mut() {
*at = stale;
}
}
async fn resolve(&self, path: &str) -> io::Result<Child> {
let (parent, name) = split_last(path);
let children = self.list_dir(&parent).await?;
children
.iter()
.find(|c| same_name(&c.vfs_name, &name))
.cloned()
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))
}
}
impl GdriveFs {
async fn serves_json(&self, path: &Path) -> bool {
let Ok(path) = vpath(path) else { return false };
self.resolve(&path)
.await
.is_ok_and(|c| matches!(c.serves, Serves::Native(..)))
}
async fn read_window(
&self,
path: &Path,
range: Option<std::ops::Range<u64>>,
) -> io::Result<Vec<u8>> {
let path = vpath(path)?;
let path = path.as_str();
if path == "/" {
return Err(io::Error::other("is a directory: /"));
}
let child = self.resolve(path).await?;
if child.kind.is_dir() {
return Err(io::Error::other(format!("is a directory: {path}")));
}
match child.serves {
Serves::Original => {
let Some(r) = range else {
return self
.accessor
.download(&child.id, None)
.await
.map_err(not_found_or_backend);
};
let want = r.end.saturating_sub(r.start);
if want == 0 {
return Ok(Vec::new());
}
let (at, bytes) = self.span(&child.id, r.start, want).await?;
Ok(slice(&bytes, Some(r.start - at..r.end.saturating_sub(at))))
}
Serves::Native(api) => {
let bytes = self.rendered_json(&child, api).await?;
Ok(slice(&bytes, range))
}
Serves::Nothing => Err(io::ErrorKind::NotFound.into()),
}
}
}
impl FileSystem for GdriveFs {
fn stat<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Stat>> {
Box::pin(async move {
let path = vpath(path)?;
if path == "/" {
return Ok(Stat::new(DirentKind::Dir, 0));
}
let child = self.resolve(&path).await?;
let size = match (&child.serves, child.size) {
(Serves::Native(..), _) => self.remembered_len(&child).await,
_ => None,
};
Ok(Stat {
kind: kind_of(&child),
size: size.unwrap_or_else(|| entry_size(&child)),
mtime: child.mtime,
created: child.created,
..Stat::new(kind_of(&child), 0)
})
})
}
fn list<'a>(&'a self, path: &'a Path) -> BoxFuture<'a, io::Result<Vec<Dirent>>> {
Box::pin(async move {
let path = vpath(path)?;
let children = self.list_dir(&path).await?;
Ok(children.iter().map(dirent_for).collect())
})
}
fn read_at<'a>(
&'a self,
path: &'a Path,
buf: &'a mut [u8],
offset: u64,
) -> BoxFuture<'a, io::Result<usize>> {
Box::pin(async move {
if buf.is_empty() {
return Ok(0);
}
let want = buf.len() as u64;
let bytes = self
.read_window(path, Some(offset..offset.saturating_add(want)))
.await?;
let n = bytes.len().min(buf.len());
buf[..n].copy_from_slice(&bytes[..n]);
if n == buf.len() {
return Ok(n);
}
let end = offset.saturating_add(n as u64);
if end >= UNKNOWN_LENGTH_SIZE || !self.serves_json(path).await {
return Ok(n);
}
let pad = ((UNKNOWN_LENGTH_SIZE - end) as usize).min(buf.len() - n);
buf[n..n + pad].fill(b' ');
let mut at = end + (PAD_LINE - 1 - end % PAD_LINE);
while at < end + pad as u64 {
buf[n + (at - end) as usize] = b'\n';
at += PAD_LINE;
}
Ok(n + pad)
})
}
}
fn entry_size(c: &Child) -> u64 {
match (c.kind.is_dir(), c.size) {
(true, _) => 0,
(_, Some(n)) => n,
(_, None) => UNKNOWN_LENGTH_SIZE,
}
}
fn kind_of(c: &Child) -> DirentKind {
if c.kind.is_dir() {
DirentKind::Dir
} else {
DirentKind::File
}
}
fn dirent_for(c: &Child) -> Dirent {
Dirent::with_stat(
c.vfs_name.clone(),
Stat {
size: entry_size(c),
mtime: c.mtime,
created: c.created,
..Stat::new(kind_of(c), 0)
},
)
}
fn not_found_or_backend(e: anyhow::Error) -> io::Error {
let is_404 = e
.downcast_ref::<reqwest::Error>()
.and_then(reqwest::Error::status)
== Some(reqwest::StatusCode::NOT_FOUND);
if is_404 {
io::Error::from(io::ErrorKind::NotFound)
} else {
io::Error::other(format!("{e:#}"))
}
}
fn child_from_file(f: &Value) -> Option<Child> {
let name = sanitize_name(f.get("name")?.as_str()?);
let id = f.get("id")?.as_str()?.to_string();
let mime = f.get("mimeType").and_then(|m| m.as_str()).unwrap_or("");
let drive_id = f.get("driveId").and_then(|d| d.as_str()).map(String::from);
let (mtime, created) = (time_field(f, "modifiedTime"), time_field(f, "createdTime"));
if mime == FOLDER_MIME {
return Some(Child {
vfs_name: name,
id,
drive_id,
kind: GKind::Folder,
mtime,
created,
serves: Serves::Nothing,
size: None,
});
}
let (vfs_name, serves, size) = if has_original_bytes(mime) {
let size = f
.get("size")
.and_then(|s| s.as_str())
.and_then(|s| s.parse::<u64>().ok());
(name, Serves::Original, size)
} else {
let (api, suffix) = native_kind(mime)?;
(format!("{name}{suffix}"), Serves::Native(api), None)
};
Some(Child {
vfs_name,
id,
drive_id,
kind: GKind::File,
mtime,
created,
serves,
size,
})
}
fn fold_values(workbook: &mut Value, batch: &Value, requested: &[String]) {
let mut by_title: HashMap<String, Value> = HashMap::new();
for vr in batch
.get("valueRanges")
.and_then(|r| r.as_array())
.into_iter()
.flatten()
{
let Some(title) = vr.get("range").and_then(|r| r.as_str()).map(range_title) else {
continue;
};
let values = vr.get("values").cloned().unwrap_or(Value::Array(vec![]));
by_title.insert(title, values);
}
let asked: HashSet<&str> = requested.iter().map(String::as_str).collect();
let mut budget = GRID_BYTES_BUDGET;
for tab in workbook
.get_mut("sheets")
.and_then(|s| s.as_array_mut())
.into_iter()
.flatten()
{
let title = tab
.pointer("/properties/title")
.and_then(|t| t.as_str())
.map(str::to_string);
let Some(obj) = tab.as_object_mut() else {
continue;
};
let note = |reason: &str| serde_json::json!({ "reason": reason });
let Some(title) = title else {
obj.insert(
"valuesOmitted".into(),
note("this sheet has no title, so its cells cannot be addressed"),
);
continue;
};
if !asked.contains(title.as_str()) {
obj.insert(
"valuesOmitted".into(),
serde_json::json!({
"reason": "past the tab cap, so its values were never requested",
"tabCap": MAX_TABS,
}),
);
continue;
}
let Some(values) = by_title.get(&title) else {
obj.insert(
"valuesOmitted".into(),
note("the values request returned nothing for this sheet"),
);
continue;
};
let cost = served_len(values);
if cost > budget {
obj.insert(
"valuesOmitted".into(),
serde_json::json!({
"reason": "over the size budget",
"bytes": cost,
"budgetLeft": budget,
}),
);
continue;
}
budget -= cost;
obj.insert("values".into(), values.clone());
}
}
fn range_title(range: &str) -> String {
let sheet = match range.rsplit_once('!') {
Some((sheet, _)) => sheet,
None => range,
};
match sheet.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
Some(inner) => inner.replace("''", "'"),
None => sheet.to_string(),
}
}
fn pretty(v: &Value) -> anyhow::Result<Vec<u8>> {
let mut bytes = serde_json::to_vec_pretty(v)?;
bytes.push(b'\n');
Ok(bytes)
}
fn tab_titles(workbook: &Value) -> Vec<String> {
workbook
.get("sheets")
.and_then(|s| s.as_array())
.into_iter()
.flatten()
.filter_map(|t| t.pointer("/properties/title")?.as_str())
.map(str::to_string)
.take(MAX_TABS)
.collect()
}
fn served_len(v: &Value) -> u64 {
struct Counting(u64);
impl std::io::Write for Counting {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0 += buf.len() as u64;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut c = Counting(0);
serde_json::to_writer_pretty(&mut c, v)
.map(|()| c.0)
.unwrap_or(0)
}
fn time_field(f: &Value, key: &str) -> Option<std::time::SystemTime> {
rfc3339_to_systemtime(f.get(key)?.as_str()?)
}
fn rfc3339_to_systemtime(s: &str) -> Option<std::time::SystemTime> {
let secs = chrono::DateTime::parse_from_rfc3339(s).ok()?.timestamp();
(secs >= 0).then(|| std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64))
}
fn sanitize_name(name: &str) -> String {
let cleaned: String = name
.trim()
.chars()
.map(|c| {
if c == '/' || c == '\\' || c.is_control() {
'_'
} else {
c
}
})
.collect();
match cleaned.as_str() {
"" | "." | ".." => "untitled".to_string(),
_ => cleaned,
}
}
fn served_name_len(name: &str) -> usize {
name.nfd().map(char::len_utf8).sum()
}
fn shorten_for_tag(name: &str, serves: Serves, tag: &str) -> String {
let (stem, ext) = split_extension(name, serves);
let fixed = served_name_len(ext) + 1 + tag.len();
let mut stem: String = stem.to_string();
while !stem.is_empty() && served_name_len(&stem) + fixed > NAME_BUDGET {
stem.pop();
}
format!("{stem}_{tag}{ext}")
}
fn disambiguate(children: &mut [Child]) {
let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
for (i, c) in children.iter().enumerate() {
groups
.entry(c.vfs_name.nfc().collect())
.or_default()
.push(i);
}
for idxs in groups.into_values() {
let short: HashSet<&str> = idxs.iter().map(|&i| id_tag(&children[i].id)).collect();
let whole = short.len() != idxs.len();
for i in idxs {
let tag = if whole {
children[i].id.clone()
} else {
id_tag(&children[i].id).to_string()
};
let tagged = shorten_for_tag(&children[i].vfs_name, children[i].serves, &tag);
children[i].vfs_name = tagged;
}
}
let mut seen: HashSet<String> = HashSet::new();
let mut order: Vec<usize> = (0..children.len()).collect();
order.sort_by(|&a, &b| children[a].id.cmp(&children[b].id));
for i in order {
if seen.insert(children[i].vfs_name.nfc().collect()) {
continue;
}
let (stem, ext) = split_extension(&children[i].vfs_name, children[i].serves);
let mut n = 2;
let renamed = loop {
let cand = format!("{stem} ({n}){ext}");
if seen.insert(cand.nfc().collect::<String>()) {
break cand;
}
n += 1;
};
children[i].vfs_name = renamed;
}
}
fn id_tag(id: &str) -> &str {
&id[id.len().saturating_sub(ID_TAG_LEN)..]
}
fn split_extension(name: &str, serves: Serves) -> (&str, &str) {
if serves == Serves::Nothing {
return (name, "");
}
if let Serves::Native(api) = serves {
let suffix = NATIVE_KINDS
.iter()
.find(|(_, a, _)| *a == api)
.map(|(_, _, s)| *s)
.unwrap_or("");
if let Some(stem) = name.strip_suffix(suffix) {
return (stem, suffix);
}
}
match name.rsplit_once('.') {
Some((stem, ext))
if !stem.is_empty()
&& ext.len() <= 8
&& ext.chars().all(|c| c.is_ascii_alphanumeric()) =>
{
(stem, &name[stem.len()..])
}
_ => (name, ""),
}
}
fn unique_name(name: &str, existing: &HashSet<String>) -> String {
let taken = |n: &str| existing.contains(&n.nfc().collect::<String>());
if !taken(name) {
return name.to_string();
}
let mut candidate = format!("{name} [Shared Drive]");
let mut suffix = 2;
while taken(&candidate) {
candidate = format!("{name} [Shared Drive {suffix}]");
suffix += 1;
}
candidate
}
fn same_name(a: &str, b: &str) -> bool {
a == b || a.nfc().eq(b.nfc())
}
fn vpath(path: &Path) -> io::Result<String> {
let mut parts: Vec<&str> = Vec::new();
for comp in path.components() {
match comp {
std::path::Component::RootDir | std::path::Component::CurDir => {}
std::path::Component::Normal(name) => parts.push(
name.to_str()
.ok_or(io::Error::from(io::ErrorKind::InvalidFilename))?,
),
std::path::Component::ParentDir | std::path::Component::Prefix(_) => {
return Err(io::ErrorKind::InvalidFilename.into());
}
}
}
Ok(format!("/{}", parts.join("/")))
}
fn split_last(path: &str) -> (String, String) {
let p = path.trim_end_matches('/');
match p.rsplit_once('/') {
Some((parent, name)) => {
let parent = if parent.is_empty() {
"/".to_string()
} else {
parent.to_string()
};
(parent, name.to_string())
}
None => ("/".to_string(), p.to_string()),
}
}
fn slice(data: &[u8], range: Option<std::ops::Range<u64>>) -> Vec<u8> {
match range {
Some(r) => {
debug_assert!(r.end >= r.start, "callers hand this a forward range");
let start = (r.start as usize).min(data.len());
let end = (r.end as usize).min(data.len());
data[start..end].to_vec()
}
None => data.to_vec(),
}
}
#[cfg(test)]
#[path = "gdrive_tests.rs"]
mod tests;