batbox_file/
lib.rs

1//! Loading files
2//!
3//! Since [std::fs] is not working on the web, use this for consistency
4#![warn(missing_docs)]
5
6use anyhow::anyhow;
7use futures::prelude::*;
8use serde::de::DeserializeOwned;
9#[cfg(not(target_arch = "wasm32"))]
10use std::pin::Pin;
11#[cfg(target_arch = "wasm32")]
12use wasm_bindgen::prelude::*;
13#[cfg(target_arch = "wasm32")]
14use wasm_bindgen_futures::JsFuture;
15
16/// Load a file at given path, returning an async reader as result
17///
18/// Supports both files and urls
19pub async fn load(path: impl AsRef<std::path::Path>) -> anyhow::Result<impl AsyncBufRead> {
20    let path = path.as_ref();
21    #[cfg(target_arch = "wasm32")]
22    {
23        let fetch: JsFuture = web_sys::window()
24            .expect("window unavailable")
25            .fetch_with_str(path.to_str().expect("path is not a valid str"))
26            .into();
27        let response: web_sys::Response = match fetch.await {
28            Ok(response) => response.unchecked_into(),
29            Err(e) => anyhow::bail!("{e:?}"),
30        };
31        let status = http::StatusCode::from_u16(response.status())?;
32        if !status.is_success() {
33            anyhow::bail!("Http status: {status}");
34        }
35        let body = response.body().expect("response without body?");
36        Ok(futures::io::BufReader::new(read_stream(body)))
37    }
38    #[cfg(target_os = "android")]
39    if batbox_android::file_mode() == batbox_android::FileMode::Assets {
40        // Maybe if starts with "asset://"??
41        let app = batbox_android::app();
42        let asset_manager = app.asset_manager();
43        let path = path.to_str().expect("Path expected to be a utf-8 str");
44        let path = path.strip_prefix("./").unwrap();
45        let path = std::ffi::CString::new(path).expect("Paths should not have null bytes");
46        let mut asset = asset_manager
47            .open(path.as_c_str())
48            .ok_or(anyhow!("Asset not found"))?;
49
50        struct ReadAsAsync<T>(Box<T>);
51
52        impl<T: std::io::Read> AsyncRead for ReadAsAsync<T> {
53            fn poll_read(
54                mut self: Pin<&mut Self>,
55                _: &mut std::task::Context<'_>,
56                buf: &mut [u8],
57            ) -> std::task::Poll<std::io::Result<usize>> {
58                std::task::Poll::Ready(std::io::Read::read(&mut self.0, buf))
59            }
60
61            fn poll_read_vectored(
62                mut self: Pin<&mut Self>,
63                _: &mut std::task::Context<'_>,
64                bufs: &mut [std::io::IoSliceMut<'_>],
65            ) -> std::task::Poll<std::io::Result<usize>> {
66                std::task::Poll::Ready(std::io::Read::read_vectored(&mut self.0, bufs))
67            }
68        }
69
70        impl<T: std::io::BufRead> AsyncBufRead for ReadAsAsync<T> {
71            fn poll_fill_buf(
72                mut self: Pin<&mut Self>,
73                _: &mut std::task::Context<'_>,
74            ) -> std::task::Poll<std::io::Result<&[u8]>> {
75                std::task::Poll::Ready(std::io::BufRead::fill_buf(&mut self.get_mut().0))
76            }
77
78            fn consume(mut self: Pin<&mut Self>, amt: usize) {
79                std::io::BufRead::consume(&mut self.0, amt)
80            }
81        }
82
83        return Ok(
84            Box::pin(ReadAsAsync(Box::new(std::io::BufReader::new(asset))))
85                as Pin<Box<dyn AsyncBufRead>>,
86        );
87    }
88    #[cfg(not(target_arch = "wasm32"))]
89    match path
90        .to_str()
91        .and_then(|path| url::Url::parse(path).ok())
92        .filter(|url| matches!(url.scheme(), "http" | "https"))
93    {
94        Some(url) => {
95            log::debug!("{:?}", url.scheme());
96            let request = reqwest::get(url);
97            let request = async_compat::Compat::new(request); // Because of tokio inside reqwest
98            let response = request.await?;
99            let status = response.status();
100            if !status.is_success() {
101                anyhow::bail!("Http status: {status}");
102            }
103            let reader = response
104                .bytes_stream()
105                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
106                .into_async_read();
107            let reader = futures::io::BufReader::new(reader);
108            Ok(Box::pin(reader) as Pin<Box<dyn AsyncBufRead>>)
109        }
110        None => {
111            let file = async_std::fs::File::open(path).await?;
112            let reader = futures::io::BufReader::new(file);
113            Ok(Box::pin(reader) as Pin<Box<dyn AsyncBufRead>>)
114        }
115    }
116}
117
118/// Turns web_sys::ReadableStream into AsyncRead
119#[cfg(target_arch = "wasm32")]
120pub fn read_stream(stream: web_sys::ReadableStream) -> impl AsyncRead {
121    let stream = wasm_streams::ReadableStream::from_raw(stream.unchecked_into());
122
123    fn js_to_string(js_value: &JsValue) -> Option<String> {
124        js_value.as_string().or_else(|| {
125            js_sys::Object::try_from(js_value)
126                .map(|js_object| js_object.to_string().as_string().unwrap_throw())
127        })
128    }
129    fn js_to_io_error(js_value: JsValue) -> std::io::Error {
130        let message = js_to_string(&js_value).unwrap_or_else(|| "Unknown error".to_string());
131        std::io::Error::new(std::io::ErrorKind::Other, message)
132    }
133
134    // TODO: BYOB not supported, not working, wot?
135    // let reader = stream.into_async_read();
136    stream
137        .into_stream()
138        .map(|result| match result {
139            Ok(chunk) => Ok(chunk.unchecked_into::<js_sys::Uint8Array>().to_vec()),
140            Err(e) => Err(js_to_io_error(e)),
141        })
142        .into_async_read()
143}
144
145/// Load file as a vec of bytes
146pub async fn load_bytes(path: impl AsRef<std::path::Path>) -> anyhow::Result<Vec<u8>> {
147    let mut buf = Vec::new();
148    load(path).await?.read_to_end(&mut buf).await?;
149    Ok(buf)
150}
151
152/// Load file as a string
153pub async fn load_string(path: impl AsRef<std::path::Path>) -> anyhow::Result<String> {
154    let mut buf = String::new();
155    load(path).await?.read_to_string(&mut buf).await?;
156    Ok(buf)
157}
158
159/// Load file and deserialize into given type using deserializer based on extension
160///
161/// Supports:
162/// - json
163/// - toml
164/// - ron
165pub async fn load_detect<T: DeserializeOwned>(
166    path: impl AsRef<std::path::Path>,
167) -> anyhow::Result<T> {
168    let path = path.as_ref();
169    let ext = path
170        .extension()
171        .ok_or(anyhow!("Expected to have extension"))?;
172    let ext = ext.to_str().ok_or(anyhow!("Extension is not valid str"))?;
173    let data = load_bytes(path).await?;
174    let value = match ext {
175        "json" => serde_json::from_reader(data.as_slice())?,
176        "toml" => toml::from_str(std::str::from_utf8(&data)?)?,
177        "ron" => ron::de::from_bytes(&data)?,
178        _ => anyhow::bail!("{ext:?} is unsupported"),
179    };
180    Ok(value)
181}
182
183/// Load json file and deserialize into given type
184pub async fn load_json<T: DeserializeOwned>(
185    path: impl AsRef<std::path::Path>,
186) -> anyhow::Result<T> {
187    let json: String = load_string(path).await?;
188    let value = serde_json::from_str(&json)?;
189    Ok(value)
190}