Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Laux
907728731e feat: webxdc: fallback to index.html if it exists when loading directory
This makes it easier for developers and users that port webapps or even
vibecode normal websites. Those may rely on the webserver behavior of
loading an index.html when navigating to a directory.

Before this pr this did not work, with this pr we have a better out of
the box experience.

Still we may want to document that links ending in `dir/index.html` are
more reliable than just linking to `dir/` - atleast in other host app
implementations
2026-06-03 19:22:28 +02:00
5 changed files with 71 additions and 11 deletions

4
Cargo.lock generated
View File

@@ -4735,9 +4735,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.40.1"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
"memchr",
]

View File

@@ -35,7 +35,6 @@ struct Statistics {
core_version: String,
number_of_transports: usize,
key_create_timestamps: Vec<u32>,
number_of_keys: u32,
/// OpenPGP version of the key.
key_version: u8,
key_algorithm: String,
@@ -356,11 +355,6 @@ async fn get_stats(context: &Context) -> Result<String> {
// `key_create_timestamps` is a `Vec` for historical reasons,
// support for using multiple keys is being phased out.
let key_create_timestamps: Vec<u32> = vec![self_public_key.created_at().as_secs()];
let number_of_keys: u32 = context
.sql
.query_get_value("SELECT COUNT(*) FROM keypairs", ())
.await?
.unwrap_or(0);
let sending_enabled_timestamps =
get_timestamps(context, "stats_sending_enabled_events").await?;
@@ -371,7 +365,6 @@ async fn get_stats(context: &Context) -> Result<String> {
core_version: DC_VERSION_STR.to_string(),
number_of_transports: context.count_transports().await?,
key_create_timestamps,
number_of_keys,
key_version: self_public_key.primary_key.version().into(),
key_algorithm: format!("{:?}", self_public_key.algorithm()),
pubkey_size: DcKey::to_bytes(&self_public_key).len(),

View File

@@ -847,8 +847,11 @@ fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
}
async fn get_blob(archive: &mut SeekZipFileReader<BufReader<File>>, name: &str) -> Result<Vec<u8>> {
let (i, _) =
let (i, entry) =
find_zip_entry(archive.file(), name).ok_or_else(|| anyhow!("no entry found for {name}"))?;
if entry.dir()? {
bail!("'{name}' is a directory not a file.")
}
let mut reader = archive.reader_with_entry(i).await?;
let mut buf = Vec::new();
reader.read_to_end_checked(&mut buf).await?;
@@ -903,7 +906,28 @@ impl Message {
));
}
get_blob(&mut archive, name).await
let result = get_blob(&mut archive, name).await;
// not found and no extension, then assume directory and try index.html
// this mimics how webservers behave.
if result.is_err() && !name.contains('.') {
let base = if name.ends_with('/') {
name.to_string()
} else {
format!("{name}/")
};
// ignore first slash. So that requesting "" for index.html works
let base = base.trim_start_matches('/');
let fallbacks = [format!("{base}index.html"), format!("{base}index.htm")];
for fallback in &fallbacks {
let result = get_blob(&mut archive, fallback).await;
if result.is_ok() {
return result;
}
}
result // return orginal error to the path that was requested, not the fallback
} else {
result
}
}
/// Return info from manifest.toml or from fallbacks.

View File

@@ -1144,6 +1144,49 @@ async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_blob_indexhtml_fallback() -> Result<()> {
let t = &TestContext::new_alice().await;
let chat_id = create_group(t, "foo").await?;
let instance = {
let mut instance = create_webxdc_instance(
t,
"indexhtml-fallback.xdc",
include_bytes!("../../test-data/webxdc/indexhtml-fallback.xdc"),
)?;
let instance_msg_id = send_msg(t, chat_id, &mut instance).await?;
assert_eq!(instance.viewtype, Viewtype::Webxdc);
Message::load_from_db(t, instance_msg_id).await?
};
// "../" links that go back should work
assert!(instance.get_webxdc_blob(t, "").await.is_ok());
// test falling back to index.html
assert!(instance.get_webxdc_blob(t, "/alpha").await.is_ok());
assert!(instance.get_webxdc_blob(t, "/alpha/").await.is_ok());
// test falling back to index.htm
assert!(instance.get_webxdc_blob(t, "/beta").await.is_ok());
assert!(instance.get_webxdc_blob(t, "/beta/").await.is_ok());
// test that original error is still there when there is no index.htm(l) file
assert!(instance.get_webxdc_blob(t, "/control").await.is_err());
println!("{:?}", instance.get_webxdc_blob(t, "/control/").await);
println!(
"{:?}",
instance.get_webxdc_blob(t, "/control/were.html").await
);
assert!(instance.get_webxdc_blob(t, "/control/").await.is_err());
assert!(
!instance
.get_webxdc_blob(t, "/control/")
.await
.expect_err("error expected because there is no index.html")
.to_string()
.contains("control/index.html")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webxdc_manifest() -> Result<()> {
let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes());

Binary file not shown.