Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/event-spans
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog-crabnebula committed Nov 22, 2023
2 parents 262aa2a + 88b4fca commit 1d8af1d
Show file tree
Hide file tree
Showing 37 changed files with 927 additions and 359 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

297 changes: 274 additions & 23 deletions devtools/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use async_stream::try_stream;
use bytes::BytesMut;
use futures::{FutureExt, Stream, TryStreamExt};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::path::{Component, PathBuf};
use std::sync::Arc;
use tauri::http::header::HeaderValue;
use tauri::{AppHandle, Runtime};
Expand Down Expand Up @@ -218,18 +218,39 @@ impl<R: Runtime> wire::sources::sources_server::Sources for SourcesService<R> {
req: Request<EntryRequest>,
) -> Result<Response<Self::ListEntriesStream>, Status> {
tracing::debug!("list entries");
let mut cwd = std::env::current_dir()?;
cwd.push(req.into_inner().path);

let stream = self.list_entries_inner(cwd).or_else(|err| async move {
tracing::error!("List Entries failed with error {err:?}");
if self.app_handle.asset_resolver().iter().count() == 0 {
let path = PathBuf::from(req.into_inner().path);

// TODO set the health service status to NotServing here
// deny requests that contain special path components, like root dir, parent dir,
// or weird windows ones. Only plain old regular, relative paths.
if !path
.components()
.all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
{
return Err(Status::not_found("file with the specified path not found"));
}

Err(Status::internal("boom"))
});
let mut cwd = std::env::current_dir()?;
cwd.push(path);

Ok(Response::new(Box::pin(stream)))
let stream = self.list_entries_from_dir(cwd).or_else(|err| async move {
tracing::error!("List Entries failed with error {err:?}");
// TODO set the health service status to NotServing here
Err(Status::internal("boom"))
});
Ok(Response::new(Box::pin(stream)))
} else {
let path = req.into_inner().path.trim_start_matches('.').to_string();
let stream = self
.list_entries_from_assets(path)
.or_else(|err| async move {
tracing::error!("List Entries failed with error {err:?}");
// TODO set the health service status to NotServing here
Err(Status::internal("boom"))
});
Ok(Response::new(Box::pin(stream)))
}
}

type GetEntryBytesStream = BoxStream<Chunk>;
Expand All @@ -238,28 +259,103 @@ impl<R: Runtime> wire::sources::sources_server::Sources for SourcesService<R> {
&self,
req: Request<EntryRequest>,
) -> Result<Response<Self::GetEntryBytesStream>, Status> {
let mut path = std::env::current_dir()?;
path.push(req.into_inner().path);
let entry_path = req.into_inner().path;
let asset_path = entry_path.trim_start_matches('.');

if let Some(asset) = self
.app_handle
.asset_resolver()
.iter()
.find(|(path, _bytes)| path == &&asset_path)
// decompress the asset
.and_then(|(path, _bytes)| self.app_handle.asset_resolver().get(path.to_string()))
{
let chunks = asset
.bytes
.chunks(512)
.map(|b| {
Ok(Chunk {
bytes: bytes::Bytes::copy_from_slice(b),
})
})
.collect::<Vec<_>>();
let stream = futures::stream::iter(chunks);
Ok(Response::new(Box::pin(stream)))
} else {
let entry_path = PathBuf::from(entry_path);
// deny requests that contain special path components, like root dir, parent dir,
// or weird windows ones. Only plain old regular, relative paths.
if !entry_path
.components()
.all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
{
return Err(Status::not_found("file with the specified path not found"));
}

let mut path = std::env::current_dir()?;
path.push(entry_path);

let stream = try_stream! {
use tokio::io::AsyncReadExt;
let mut file = tokio::fs::File::open(path).await?;
let mut buf = BytesMut::with_capacity(512);
let stream = try_stream! {
use tokio::io::AsyncReadExt;
let mut file = tokio::fs::File::open(path).await?;
let mut buf = BytesMut::with_capacity(512);

while let Ok(n) = file.read_buf(&mut buf).await {
if n == 0 {
break;
while let Ok(n) = file.read_buf(&mut buf).await {
if n == 0 {
break;
}
yield Chunk { bytes: buf.split().freeze() };
}
yield Chunk { bytes: buf.split().freeze() };
}
};
};

Ok(Response::new(Box::pin(stream)))
Ok(Response::new(Box::pin(stream)))
}
}
}

impl<R: Runtime> SourcesService<R> {
fn list_entries_inner(&self, root: PathBuf) -> impl Stream<Item = crate::Result<Entry>> {
fn list_entries_from_assets(&self, root: String) -> impl Stream<Item = crate::Result<Entry>> {
let resolver = self.app_handle.asset_resolver();

let mut entries: Vec<Entry> = Vec::new();
for (asset_path, _bytes) in self.app_handle.asset_resolver().iter() {
// strip `/` prefix
let path: String = asset_path.chars().skip(1).collect();

let mut entry_path = path;
let mut entry_type = FileType::FILE;

if root.is_empty() {
if let Some((dir, _path)) = entry_path.split_once('/') {
entry_path = dir.to_string();
entry_type = FileType::DIR;
}
} else if let Some(p) = entry_path.strip_prefix(&format!("{root}/")) {
if let Some((dir, _path)) = p.split_once('/') {
entry_path = dir.to_string();
entry_type = FileType::DIR;
} else {
entry_path = p.to_string();
}
} else {
// asset does not belong to root
continue;
}

if !entries.iter().any(|e| e.path == entry_path) {
entries.push(Entry {
path: entry_path,
// we use resolver.get since it increases the size sometimes (e.g. injecting CSP on HTML files)
size: resolver.get(asset_path.to_string()).unwrap().bytes.len() as u64,
file_type: (FileType::ASSET | entry_type).bits(),
});
}
}

futures::stream::iter(entries.into_iter().map(Ok))
}

fn list_entries_from_dir(&self, root: PathBuf) -> impl Stream<Item = crate::Result<Entry>> {
let app_handle = self.app_handle.clone();

try_stream! {
Expand Down Expand Up @@ -323,8 +419,10 @@ impl<R: Runtime> metadata_server::Metadata for MetaService<R> {
#[cfg(test)]
mod test {
use super::*;
use futures::StreamExt;
use std::time::SystemTime;
use tauri_devtools_wire_format::instrument::instrument_server::Instrument;
use tauri_devtools_wire_format::sources::sources_server::Sources;
use tauri_devtools_wire_format::tauri::tauri_server::Tauri;

#[tokio::test]
Expand Down Expand Up @@ -390,4 +488,157 @@ mod test {

assert!(matches!(cmd, Command::Instrument(_)));
}

#[tokio::test]
async fn sources_list_entries() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let stream = srv
.list_entries(Request::new(EntryRequest {
path: ".".to_string(),
}))
.await
.unwrap();

// this will list this crates directory, so should produce a `Cargo.toml` and `src` entry
let entries: Vec<_> = stream.into_inner().collect().await;
assert_eq!(entries.len(), 2);

assert_eq!(entries[0].as_ref().unwrap().file_type, 1 << 1);
assert_eq!(entries[0].as_ref().unwrap().path, "Cargo.toml");

assert_eq!(entries[1].as_ref().unwrap().file_type, 1 << 0);
assert_eq!(entries[1].as_ref().unwrap().path, "src");
}

#[tokio::test]
async fn sources_list_entries_root() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let res = srv
.list_entries(Request::new(EntryRequest {
path: "/".to_string(),
}))
.await;

assert!(res.is_err(), "requesting the root path should fail");

let res = srv
.list_entries(Request::new(EntryRequest {
path: "/foo/bar/this".to_string(),
}))
.await;

assert!(res.is_err(), "requesting the root path should fail")
}

#[tokio::test]
async fn sources_list_entries_parent() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let res = srv
.list_entries(Request::new(EntryRequest {
path: "../".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail");

let res = srv
.list_entries(Request::new(EntryRequest {
path: "foo/bar/../this".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail");

let res = srv
.list_entries(Request::new(EntryRequest {
path: "..".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail")
}

#[tokio::test]
async fn sources_get_bytes() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let stream = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "./Cargo.toml".to_string(),
}))
.await
.unwrap();

// this will list this crates directory, so should produce a `Cargo.toml` and `src` entry
let chunks: Vec<_> = stream.into_inner().collect().await;

let mut buf = Vec::new();

for chunk in chunks {
buf.extend_from_slice(&chunk.unwrap().bytes);
}

// we don't want to hard code the exact size of Cargo.toml, that would be flaky
// but it should definitely be larger than zero
assert!(buf.len() > 0);
}

#[tokio::test]
async fn sources_get_bytes_root() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let res = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "/".to_string(),
}))
.await;

assert!(res.is_err(), "requesting the root path should fail");

let res = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "/foo/bar/this".to_string(),
}))
.await;

assert!(res.is_err(), "requesting the root path should fail")
}

#[tokio::test]
async fn sources_get_bytes_parent() {
let app_handle = tauri::test::mock_app().handle();
let srv = SourcesService { app_handle };

let res = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "../".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail");

let res = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "foo/bar/../this".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail");

let res = srv
.get_entry_bytes(Request::new(EntryRequest {
path: "..".to_string(),
}))
.await;

assert!(res.is_err(), "requesting an absolute path should fail")
}
}
Loading

0 comments on commit 1d8af1d

Please sign in to comment.