Rewild Your Web
18
fork

Configure Feed

Select the types of activity you want to include in your feed.

tiles: initial support

Signed-off-by: webbeef <me@webbeef.org>

webbeef e8332894 dce26e74

+567 -52
+4 -8
Cargo.lock
··· 2936 2936 [[package]] 2937 2937 name = "form_urlencoded" 2938 2938 version = "1.2.2" 2939 - source = "registry+https://github.com/rust-lang/crates.io-index" 2940 - checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 2939 + source = "git+https://github.com/webbeef/rust-url.git?branch=beaver#2bc19f7b8b57dd5dbab417cc4d050ebf8c0b6468" 2941 2940 dependencies = [ 2942 2941 "percent-encoding", 2943 2942 ] ··· 5047 5046 [[package]] 5048 5047 name = "idna" 5049 5048 version = "1.1.0" 5050 - source = "registry+https://github.com/rust-lang/crates.io-index" 5051 - checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 5049 + source = "git+https://github.com/webbeef/rust-url.git?branch=beaver#2bc19f7b8b57dd5dbab417cc4d050ebf8c0b6468" 5052 5050 dependencies = [ 5053 5051 "idna_adapter", 5054 5052 "smallvec", ··· 7520 7518 [[package]] 7521 7519 name = "percent-encoding" 7522 7520 version = "2.3.2" 7523 - source = "registry+https://github.com/rust-lang/crates.io-index" 7524 - checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 7521 + source = "git+https://github.com/webbeef/rust-url.git?branch=beaver#2bc19f7b8b57dd5dbab417cc4d050ebf8c0b6468" 7525 7522 7526 7523 [[package]] 7527 7524 name = "petgraph" ··· 12318 12315 [[package]] 12319 12316 name = "url" 12320 12317 version = "2.5.8" 12321 - source = "registry+https://github.com/rust-lang/crates.io-index" 12322 - checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 12318 + source = "git+https://github.com/webbeef/rust-url.git?branch=beaver#2bc19f7b8b57dd5dbab417cc4d050ebf8c0b6468" 12323 12319 dependencies = [ 12324 12320 "form_urlencoded", 12325 12321 "idna",
+6
Cargo.toml
··· 313 313 debug = true 314 314 lto = "thin" 315 315 codegen-units = 1 316 + 317 + [patch.crates-io] 318 + url = { git = "https://github.com/webbeef/rust-url.git", branch = "beaver" } 319 + form_urlencoded = { git = "https://github.com/webbeef/rust-url.git", branch = "beaver" } 320 + percent-encoding = { git = "https://github.com/webbeef/rust-url.git", branch = "beaver" } 321 + idna = { git = "https://github.com/webbeef/rust-url.git", branch = "beaver" }
+40 -7
patches/components/net/http_loader.rs.patch
··· 31 31 }; 32 32 use parking_lot::{Mutex, RwLock}; 33 33 use profile_traits::mem::{Report, ReportKind}; 34 - @@ -91,7 +93,7 @@ 34 + @@ -91,7 +93,8 @@ 35 35 use crate::http_cache::{ 36 36 CacheKey, CachedResourcesOrGuard, HttpCache, construct_response, invalidate, refresh, 37 37 }; 38 38 -use crate::resource_thread::{AuthCache, AuthCacheEntry}; 39 39 +use crate::resource_thread::AuthCache; 40 + +use crate::web_tiles::WebTile; 40 41 use crate::websocket_loader::start_websocket; 41 42 42 43 /// The various states an entry of the HttpCache can be in. 43 - @@ -106,6 +108,7 @@ 44 + @@ -106,6 +109,7 @@ 44 45 } 45 46 46 47 pub struct HttpState { ··· 48 49 pub hsts_list: RwLock<HstsList>, 49 50 pub cookie_jar: RwLock<CookieStorage>, 50 51 pub http_cache: HttpCache, 51 - @@ -114,9 +117,25 @@ 52 + @@ -114,9 +118,28 @@ 52 53 pub client: ServoClient, 53 54 pub override_manager: CertificateErrorOverrideManager, 54 55 pub embedder_proxy: GenericEmbedderProxy<NetToEmbedderMsg>, 55 56 + pub atproto_session: RwLock<Option<AtProtoSessionState>>, 57 + + /// The key to identify a Web Tile is (subject, rkey) of the 58 + + /// at: uri used to fetch the ing.dasl.masl record. 59 + + pub web_tiles: RwLock<FxHashMap<(String, String), WebTile>>, 56 60 } 57 61 58 62 impl HttpState { ··· 74 78 pub(crate) fn memory_reports(&self, suffix: &str, ops: &mut MallocSizeOfOps) -> Vec<Report> { 75 79 vec![ 76 80 Report { 77 - @@ -577,14 +596,40 @@ 81 + @@ -156,6 +179,28 @@ 82 + )); 83 + receiver.await.ok()? 84 + } 85 + + 86 + + pub fn register_webtile(&self, subject: &str, rkey: &str, json: &[u8]) { 87 + + if let Some(tile) = WebTile::from_tile_manifest(subject, rkey, json) { 88 + + if let Some(config_dir) = &self.config_dir { 89 + + tile.write_to(&config_dir); 90 + + } 91 + + println!( 92 + + "Registered tile, url will be tile://{}.{}/", 93 + + tile.rkey, tile.subject 94 + + ); 95 + + self.web_tiles 96 + + .write() 97 + + .insert((subject.into(), rkey.into()), tile); 98 + + } 99 + + } 100 + + 101 + + pub fn get_webtile(&self, subject: &str, rkey: &str) -> Option<WebTile> { 102 + + self.web_tiles 103 + + .read() 104 + + .get(&(subject.into(), rkey.into())) 105 + + .cloned() 106 + + } 107 + } 108 + 109 + /// Step 11 of <https://fetch.spec.whatwg.org/#concept-fetch>. 110 + @@ -577,14 +622,40 @@ 78 111 } 79 112 } 80 113 ··· 119 152 } else { 120 153 None 121 154 } 122 - @@ -1645,15 +1690,15 @@ 155 + @@ -1645,15 +1716,15 @@ 123 156 authorization_value.is_none() && 124 157 has_credentials(&current_url) 125 158 { ··· 138 171 } 139 172 } 140 173 } 141 - @@ -1858,7 +1903,7 @@ 174 + @@ -1858,7 +1929,7 @@ 142 175 }; 143 176 144 177 // Store the credentials as a proxy-authentication entry. ··· 147 180 user_name: credentials.username, 148 181 password: credentials.password, 149 182 }; 150 - @@ -1865,7 +1910,7 @@ 183 + @@ -1865,7 +1936,7 @@ 151 184 { 152 185 let mut auth_cache = context.state.auth_cache.write(); 153 186 let key = request.current_url().origin().ascii_serialization();
+3 -1
patches/components/net/lib.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -24,6 +24,11 @@ 3 + @@ -23,7 +23,13 @@ 4 + pub mod subresource_integrity; 4 5 #[cfg(feature = "test-util")] 5 6 pub mod test_util; 7 + +mod web_tiles; 6 8 mod websocket_loader; 7 9 +pub mod atproto { 8 10 + pub mod pds;
+33 -8
patches/components/net/protocols/atproto/protocol.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -0,0 +1,210 @@ 3 + @@ -0,0 +1,235 @@ 4 4 +// SPDX-License-Identifier: AGPL-3.0-or-later 5 5 + 6 6 +use std::future::{self, Future}; ··· 13 13 +use content_security_policy::percent_encoding::percent_decode_str; 14 14 +use http::header::CONTENT_TYPE; 15 15 +use http::{Method, StatusCode}; 16 - +use log::info; 16 + +use log::{error, info}; 17 17 +use net_traits::fetch::utils::http_response; 18 18 +use net_traits::request::Request; 19 - +use net_traits::response::Response; 19 + +use net_traits::response::{Response, ResponseBody}; 20 20 +use servo_url::ServoUrl; 21 21 +use sync_wrapper::SyncWrapper; 22 22 + ··· 131 131 + let client = XrpcClient::new(endpoint_url, url.clone(), &document.id, context2.clone()); 132 132 + 133 133 + if method == Method::GET { 134 - + let (response, reason) = match (collection, rkey) { 134 + + let (response, reason, is_web_tile) = match (collection, rkey) { 135 135 + (None, _) => { 136 136 + // If we have no collection, send a com.atproto.repo.describeRepo request. 137 - + (client.describe_repo().await, "Failed to describe repo") 137 + + ( 138 + + client.describe_repo().await, 139 + + "Failed to describe repo", 140 + + false, 141 + + ) 138 142 + }, 139 143 + (Some(coll), None) => { 140 144 + // If we have a collection but no rkey, send a com.atproto.repo.listRecords request. 141 - + (client.list_records(coll).await, "Failed to list records") 145 + + ( 146 + + client.list_records(coll).await, 147 + + "Failed to list records", 148 + + false, 149 + + ) 142 150 + }, 143 151 + (Some(coll), Some(rkey)) => { 144 152 + // Both collection and rkey are present, send a com.atproto.repo.getRecord request. 145 - + (client.get_record(coll, rkey).await, "Failed to get record") 153 + + ( 154 + + client.get_record(coll, rkey).await, 155 + + "Failed to get record", 156 + + coll == "ing.dasl.masl", 157 + + ) 146 158 + }, 147 159 + }; 148 - + maybe_bad_request(&url, reason, response) 160 + + let response = maybe_bad_request(&url, reason, response); 161 + + if is_web_tile { 162 + + if response.status == StatusCode::OK { 163 + + let body = response.body.lock(); 164 + + if let ResponseBody::Done(ref json) = *body { 165 + + context2 166 + + .state 167 + + .register_webtile(&subject, &rkey.unwrap(), &json); 168 + + } else { 169 + + error!("Unable to get webtile manifest!"); 170 + + } 171 + + } 172 + + } 173 + + response 149 174 + } else if method == Method::POST { 150 175 + let request = request.get_mut(); 151 176 +
+7 -2
patches/components/net/protocols/mod.rs.patch
··· 1 1 --- original 2 2 +++ modified 3 - @@ -21,13 +21,17 @@ 3 + @@ -21,13 +21,19 @@ 4 4 5 5 use crate::fetch::methods::{DoneChannel, FetchContext, RangeRequestBounds, fetch}; 6 6 ··· 9 9 mod data; 10 10 mod file; 11 11 +mod trusted; 12 + +mod web_tiles; 12 13 13 14 +use atproto::protocol::AtProtocolHandler; 14 15 use blob::BlobProtocolHander; 15 16 use data::DataProtocolHander; 16 17 use file::FileProtocolHander; 17 18 +use trusted::protocol::TrustedProtocolHandler; 19 + +use web_tiles::WebTileProtocolHandler; 18 20 19 21 type FutureResponse<'a> = Pin<Box<dyn Future<Output = Response> + Send + 'a>>; 20 22 21 - @@ -97,6 +101,12 @@ 23 + @@ -97,6 +103,15 @@ 22 24 .register("file", FileProtocolHander::default()) 23 25 .expect("Infallible"); 24 26 registry ··· 26 28 + .expect("Infallible"); 27 29 + registry 28 30 + .register("at", AtProtocolHandler::default()) 31 + + .expect("Infallible"); 32 + + registry 33 + + .register("tile", WebTileProtocolHandler::default()) 29 34 + .expect("Infallible"); 30 35 + registry 31 36 }
+164
patches/components/net/protocols/web_tiles.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,161 @@ 4 + +// SPDX-License-Identifier: AGPL-3.0-or-later 5 + + 6 + +/// Implementation of the tile:// protocol handler. 7 + +/// Example: tile://bafyreifgp34h3kkv5mohjovxkhj5di2pii3ldmqc6xdw7spsa2wbstybwi/icon.png 8 + +/// 9 + +use std::future::{self, Future}; 10 + +use std::num::NonZeroUsize; 11 + +use std::pin::Pin; 12 + +use std::sync::Arc; 13 + + 14 + +use atproto_identity::resolve::HickoryDnsResolver; 15 + +use atproto_identity::storage_lru::LruDidDocumentStorage; 16 + +use content_security_policy::percent_encoding::percent_decode_str; 17 + +use http::header::{CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderValue}; 18 + +use http::{Method, StatusCode}; 19 + +use log::error; 20 + +use net_traits::fetch::utils::http_response; 21 + +use net_traits::request::Request; 22 + +use net_traits::response::Response; 23 + +use servo_url::ServoUrl; 24 + + 25 + +use crate::atproto::pds::get_endpoint_for_subject; 26 + +use crate::atproto::xrpc::XrpcClient; 27 + +use crate::fetch::methods::{DoneChannel, FetchContext}; 28 + +use crate::protocols::ProtocolHandler; 29 + + 30 + +const WEB_TILES_CSP: &str = r#"default-src 'self' blob: data:; 31 + + script-src 'self' blob: data: 'unsafe-inline' 'wasm-unsafe-eval'; 32 + + style-src 'self' blob: data: 'unsafe-inline'; 33 + + script-src-attr 'none'; 34 + + form-src 'self'; 35 + + manifest-src 'none'; 36 + + object-src 'none'; 37 + + base-uri 'none'; 38 + + sandbox allow-downloads 39 + + allow-forms 40 + + allow-modals 41 + + allow-popups 42 + + allow-popups-to-escape-sandbox 43 + + allow-same-origin 44 + + allow-scripts"#; 45 + + 46 + +pub struct WebTileProtocolHandler { 47 + + // TODO: maybe share with the AtProtocolHandler 48 + + document_storage: LruDidDocumentStorage, 49 + + dns_resolver: Arc<HickoryDnsResolver>, 50 + +} 51 + + 52 + +impl Default for WebTileProtocolHandler { 53 + + fn default() -> Self { 54 + + Self { 55 + + document_storage: LruDidDocumentStorage::new(NonZeroUsize::new(1000).unwrap()), 56 + + dns_resolver: Arc::new(HickoryDnsResolver::create_resolver(Default::default())), 57 + + } 58 + + } 59 + +} 60 + + 61 + +fn maybe_bad_request(url: &ServoUrl, reason: &str, response: Result<Response, ()>) -> Response { 62 + + response.unwrap_or_else(|_| http_response(url.clone(), StatusCode::BAD_REQUEST, reason)) 63 + +} 64 + + 65 + +impl ProtocolHandler for WebTileProtocolHandler { 66 + + fn load( 67 + + &self, 68 + + request: &mut Request, 69 + + _done_chan: &mut DoneChannel, 70 + + context: &FetchContext, 71 + + ) -> Pin<Box<dyn Future<Output = Response> + Send>> { 72 + + let url = request.current_url(); 73 + + 74 + + let method = request.method.clone(); 75 + + if method != Method::GET { 76 + + return Box::pin(future::ready(http_response( 77 + + url, 78 + + StatusCode::BAD_REQUEST, 79 + + "Invalid method", 80 + + ))); 81 + + } 82 + + 83 + + let context2 = context.clone(); 84 + + let document_storage = self.document_storage.clone(); 85 + + let dns_resolver = Arc::clone(&self.dns_resolver); 86 + + Box::pin(async move { 87 + + // Get the host and the subject from the registered tiles 88 + + let Some(host) = url.host_str() else { 89 + + return http_response(url, StatusCode::BAD_REQUEST, "No host"); 90 + + }; 91 + + 92 + + let Ok(rkey_subject) = percent_decode_str(host).decode_utf8() else { 93 + + return http_response(url, StatusCode::BAD_REQUEST, "Host decoding error"); 94 + + }; 95 + + 96 + + // Split the rkey.subject string. 97 + + let Some((rkey, subject)) = rkey_subject.split_once('.') else { 98 + + return http_response(url, StatusCode::BAD_REQUEST, "Invalid rkey.subject format"); 99 + + }; 100 + + 101 + + let Some(tile) = context2.state.get_webtile(subject, rkey) else { 102 + + return http_response(url, StatusCode::BAD_REQUEST, "No such tile registered"); 103 + + }; 104 + + 105 + + let Ok((endpoint_url, document)) = 106 + + get_endpoint_for_subject(&tile.subject, Some(document_storage), Some(dns_resolver)) 107 + + .await 108 + + else { 109 + + return http_response( 110 + + url, 111 + + StatusCode::BAD_REQUEST, 112 + + "Failed to resolve endpoint for subject", 113 + + ); 114 + + }; 115 + + 116 + + let Some(resource) = tile.resources.get(url.path()) else { 117 + + return http_response(url, StatusCode::NOT_FOUND, "Resource not found"); 118 + + }; 119 + + 120 + + // Fetch the blob for (subject, resource.link) 121 + + let client = XrpcClient::new(endpoint_url, url.clone(), &document.id, context2.clone()); 122 + + let response = client 123 + + .get_record("com.atproto.sync.blob", &resource.link) 124 + + .await; 125 + + let mut response = maybe_bad_request(&url, "not found", response) 126 + + .internal_response 127 + + .unwrap(); 128 + + 129 + + // Adjust the headers of the internal response. 130 + + if response.status == StatusCode::OK { 131 + + let headers = &mut response.headers; 132 + + 133 + + // Remove the "attachment" content disposition. 134 + + headers.remove(CONTENT_DISPOSITION); 135 + + 136 + + // Substitute application/octet-stream with the resource's real content type. 137 + + if let Ok(header_value) = HeaderValue::from_str(&resource.content_type) { 138 + + headers.remove(CONTENT_TYPE); 139 + + headers.insert(CONTENT_TYPE, header_value); 140 + + } 141 + + 142 + + // Set the proper CSP. 143 + + headers.remove(CONTENT_SECURITY_POLICY); 144 + + let csp = WEB_TILES_CSP.replace("\n", ""); 145 + + match HeaderValue::from_str(&csp) { 146 + + Ok(csp) => { 147 + + let _ = headers.insert(CONTENT_SECURITY_POLICY, csp); 148 + + }, 149 + + Err(err) => error!("Invalid CSP: {err}"), 150 + + } 151 + + } 152 + + 153 + + *response 154 + + }) 155 + + } 156 + + 157 + + fn is_fetchable(&self) -> bool { 158 + + true 159 + + } 160 + + 161 + + fn is_secure(&self) -> bool { 162 + + true 163 + + } 164 + +}
+16 -4
patches/components/net/resource_thread.rs.patch
··· 15 15 }; 16 16 use parking_lot::{Mutex, RwLock}; 17 17 use profile_traits::mem::{ 18 - @@ -199,14 +199,17 @@ 18 + @@ -70,6 +70,7 @@ 19 + use crate::http_loader::{HttpState, http_redirect_fetch}; 20 + use crate::protocols::ProtocolRegistry; 21 + use crate::request_interceptor::RequestInterceptor; 22 + +use crate::web_tiles::WebTiles; 23 + use crate::websocket_loader::create_handshake_request; 24 + 25 + /// Load a file with CA certificate and produce a RootCertStore with the results. 26 + @@ -199,14 +200,19 @@ 19 27 let mut hsts_list = HstsList::default(); 20 28 let mut auth_cache = AuthCache::default(); 21 29 let mut cookie_jar = CookieStorage::new(150); 22 30 + let mut atproto_session = None; 31 + + let mut web_tiles = Default::default(); 23 32 if let Some(config_dir) = config_dir { 24 33 servo_base::read_json_from_file(&mut auth_cache, config_dir, "auth_cache.json"); 25 34 servo_base::read_json_from_file(&mut hsts_list, config_dir, "hsts_list.json"); 26 35 servo_base::read_json_from_file(&mut cookie_jar, config_dir, "cookie_jar.json"); 27 36 + atproto_session = AtProtoSessionState::load(config_dir); 37 + + web_tiles = WebTiles::load(config_dir); 28 38 } 29 39 30 40 let override_manager = CertificateErrorOverrideManager::new(); ··· 33 43 hsts_list: RwLock::new(hsts_list), 34 44 cookie_jar: RwLock::new(cookie_jar), 35 45 auth_cache: RwLock::new(auth_cache), 36 - @@ -219,10 +222,12 @@ 46 + @@ -219,10 +225,13 @@ 37 47 )), 38 48 override_manager, 39 49 embedder_proxy: embedder_proxy.clone(), 40 50 + atproto_session: RwLock::new(atproto_session), 51 + + web_tiles: RwLock::new(web_tiles), 41 52 }; 42 53 43 54 let override_manager = CertificateErrorOverrideManager::new(); ··· 46 57 hsts_list: RwLock::new(HstsList::default()), 47 58 cookie_jar: RwLock::new(CookieStorage::new(150)), 48 59 auth_cache: RwLock::new(AuthCache::default()), 49 - @@ -235,6 +240,7 @@ 60 + @@ -235,6 +244,8 @@ 50 61 )), 51 62 override_manager, 52 63 embedder_proxy, 53 64 + atproto_session: RwLock::new(None), 65 + + web_tiles: Default::default(), 54 66 }; 55 67 56 68 (Arc::new(http_state), Arc::new(private_http_state)) 57 - @@ -592,17 +598,18 @@ 69 + @@ -592,17 +603,18 @@ 58 70 }, 59 71 // Ignore this message as we handle it only in the reporter chan 60 72 CoreResourceMsg::CollectMemoryReport(_) => {},
+129
patches/components/net/web_tiles.rs.patch
··· 1 + --- original 2 + +++ modified 3 + @@ -0,0 +1,126 @@ 4 + +// SPDX-License-Identifier: AGPL-3.0-or-later 5 + + 6 + +use std::fs::{self, File}; 7 + +use std::path::Path; 8 + + 9 + +use log::error; 10 + +use rustc_hash::FxHashMap; 11 + +use serde::{Deserialize, Serialize}; 12 + +use serde_json::Value; 13 + +use sha2::{Digest, Sha256}; 14 + + 15 + +#[derive(Clone, Default, Deserialize, Serialize)] 16 + +pub struct TileResource { 17 + + pub link: String, 18 + + pub content_type: String, 19 + +} 20 + + 21 + +#[derive(Clone, Default, Deserialize, Serialize)] 22 + +pub struct WebTile { 23 + + pub subject: String, 24 + + pub rkey: String, 25 + + pub resources: FxHashMap<String, TileResource>, 26 + +} 27 + + 28 + +impl WebTile { 29 + + pub fn from_tile_manifest(subject: &str, rkey: &str, json: &[u8]) -> Option<Self> { 30 + + let Ok(root) = serde_json::from_slice::<Value>(json) else { 31 + + return None; 32 + + }; 33 + + 34 + + let Value::Object(ref value) = root["value"] else { 35 + + return None; 36 + + }; 37 + + 38 + + let Value::Object(ref tile) = value["tile"] else { 39 + + return None; 40 + + }; 41 + + 42 + + let Value::Object(ref doc_resources) = tile["resources"] else { 43 + + return None; 44 + + }; 45 + + 46 + + let mut resources: FxHashMap<String, TileResource> = Default::default(); 47 + + 48 + + for (path, resource) in doc_resources.iter() { 49 + + let Value::String(ref content_type) = resource["content-type"] else { 50 + + continue; 51 + + }; 52 + + 53 + + let Value::Object(ref src) = resource["src"] else { 54 + + continue; 55 + + }; 56 + + 57 + + let Value::Object(ref tref) = src["ref"] else { 58 + + continue; 59 + + }; 60 + + 61 + + let Value::String(ref link) = tref["$link"] else { 62 + + continue; 63 + + }; 64 + + 65 + + resources.insert( 66 + + path.into(), 67 + + TileResource { 68 + + link: link.to_string(), 69 + + content_type: content_type.to_string(), 70 + + }, 71 + + ); 72 + + } 73 + + 74 + + Some(Self { 75 + + subject: subject.into(), 76 + + rkey: rkey.into(), 77 + + resources, 78 + + }) 79 + + } 80 + + 81 + + /// Write to config_dir/web_tiles/tile_cid.json 82 + + /// TODO: error management 83 + + pub fn write_to(&self, config_dir: &Path) { 84 + + let path = config_dir.join("web_tiles"); 85 + + let _ = fs::create_dir_all(&path); 86 + + 87 + + let mut hasher = Sha256::new(); 88 + + hasher.update(self.subject.as_bytes()); 89 + + hasher.update(self.rkey.as_bytes()); 90 + + let result = hasher.finalize(); 91 + + 92 + + let path = path.join(format!("{result:x}.json")); 93 + + let file = File::create(path).unwrap(); 94 + + let _ = serde_json::to_writer(file, self); 95 + + } 96 + +} 97 + + 98 + +pub struct WebTiles {} 99 + + 100 + +impl WebTiles { 101 + + /// Returns a map of tile cid -> WebTile 102 + + pub fn load(config_dir: &Path) -> FxHashMap<(String, String), WebTile> { 103 + + let mut result = FxHashMap::default(); 104 + + let dir = config_dir.join("web_tiles"); 105 + + 106 + + let Ok(entries) = fs::read_dir(&dir) else { 107 + + error!("Failed to read in {}", dir.display()); 108 + + return Default::default(); 109 + + }; 110 + + 111 + + for entry in entries { 112 + + let Ok(entry) = entry else { 113 + + continue; 114 + + }; 115 + + let path = entry.path(); 116 + + if !path.is_dir() { 117 + + let Ok(file) = File::open(path) else { 118 + + continue; 119 + + }; 120 + + if let Ok(tile) = serde_json::from_reader::<File, WebTile>(file) { 121 + + println!("============ Loading tile {} {}", tile.subject, tile.rkey); 122 + + result.insert((tile.subject.clone(), tile.rkey.clone()), tile); 123 + + } 124 + + } 125 + + } 126 + + 127 + + result 128 + + } 129 + +}
-22
patches/components/url/lib.rs.patch
··· 1 - --- original 2 - +++ modified 3 - @@ -88,6 +88,19 @@ 4 - } 5 - 6 - pub fn origin(&self) -> ImmutableOrigin { 7 - + // We want trusted:// urls to have a tuple origin similar to https:// ones. 8 - + // TODO: maybe fork the url crate instead... 9 - + if self.0.scheme() == "trusted" { 10 - + use url::Origin; 11 - + 12 - + let url_str = self.as_str(); 13 - + let new_url = Url::parse(&url_str.replace("trusted://", "https://")).unwrap(); 14 - + let origin = match new_url.origin() { 15 - + Origin::Tuple(_scheme, host, port) => Origin::Tuple("trusted".into(), host, port), 16 - + Origin::Opaque(val) => Origin::Opaque(val), 17 - + }; 18 - + return ImmutableOrigin::new(origin); 19 - + } 20 - ImmutableOrigin::new(self.0.origin()) 21 - } 22 -
+164
ui/shared/lazy_loader.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + 3 + // Cache for loaded resources - stores promises immediately to handle concurrent loads 4 + const cache = new Map(); 5 + 6 + // Core loader functions 7 + const loaders = { 8 + module: (url) => import(url), 9 + 10 + script: (url) => { 11 + return new Promise((resolve, reject) => { 12 + const script = document.createElement("script"); 13 + script.src = url; 14 + script.onload = () => resolve(url); 15 + script.onerror = () => reject(new Error(`Failed to load script: ${url}`)); 16 + document.head.appendChild(script); 17 + }); 18 + }, 19 + 20 + css: (url) => { 21 + return new Promise((resolve, reject) => { 22 + const link = document.createElement("link"); 23 + link.rel = "stylesheet"; 24 + link.href = url; 25 + link.onload = () => resolve(url); 26 + link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`)); 27 + document.head.appendChild(link); 28 + }); 29 + }, 30 + 31 + json: async (url) => { 32 + try { 33 + const response = await fetch(url); 34 + if (!response.ok) { 35 + throw new Error(`Failed to fetch JSON: ${url} (${response.status})`); 36 + } 37 + return response.json(); 38 + } catch (e) { 39 + console.error(`LazyLoader.json failed for ${url}:`, e); 40 + throw e; 41 + } 42 + }, 43 + 44 + text: async (url) => { 45 + try { 46 + const response = await fetch(url); 47 + if (!response.ok) { 48 + throw new Error(`Failed to fetch text: ${url} (${response.status})`); 49 + } 50 + return response.text(); 51 + } catch (e) { 52 + console.error(`LazyLoader.text failed for ${url}:`, e); 53 + throw e; 54 + } 55 + }, 56 + }; 57 + 58 + // Parse descriptor: { module: "url" } -> ["module", "url"] 59 + function parseDescriptor(descriptor) { 60 + const entries = Object.entries(descriptor); 61 + if (entries.length !== 1) { 62 + throw new Error("Descriptor must have exactly one key"); 63 + } 64 + return entries[0]; 65 + } 66 + 67 + // Cached loader - stores promise immediately to handle concurrent loads 68 + function cachedLoad(type, url) { 69 + const key = `${type}:${url}`; 70 + if (!cache.has(key)) { 71 + // Store promise immediately, before it resolves 72 + const promise = loaders[type](url); 73 + cache.set(key, promise); 74 + } 75 + return cache.get(key); 76 + } 77 + 78 + // Load from descriptor 79 + function loadDescriptor(descriptor) { 80 + const [type, url] = parseDescriptor(descriptor); 81 + return cachedLoad(type, url); 82 + } 83 + 84 + // Builder class for chained API 85 + class LoaderChain { 86 + constructor() { 87 + this.steps = []; 88 + } 89 + 90 + load(descriptorOrArray) { 91 + this.steps.push(descriptorOrArray); 92 + return this; 93 + } 94 + 95 + then(descriptorOrArray) { 96 + return this.load(descriptorOrArray); 97 + } 98 + 99 + async all() { 100 + const results = []; 101 + for (const step of this.steps) { 102 + if (Array.isArray(step)) { 103 + // Parallel: array of descriptors 104 + const parallel = await Promise.all(step.map(loadDescriptor)); 105 + results.push(parallel); 106 + } else { 107 + // Single descriptor 108 + results.push(await loadDescriptor(step)); 109 + } 110 + } 111 + return results; 112 + } 113 + } 114 + 115 + // Manifest resolver 116 + class ManifestLoader { 117 + constructor(manifest) { 118 + this.manifest = manifest.dependencies || manifest; 119 + } 120 + 121 + async resolve(name) { 122 + const dep = this.manifest[name]; 123 + if (!dep) throw new Error(`Unknown dependency: ${name}`); 124 + 125 + if (dep.type === "group") { 126 + if (dep.parallel) { 127 + return Promise.all(dep.parallel.map((n) => this.resolve(n))); 128 + } else if (dep.sequence) { 129 + const results = []; 130 + for (const n of dep.sequence) { 131 + results.push(await this.resolve(n)); 132 + } 133 + return results; 134 + } 135 + } else { 136 + return cachedLoad(dep.type, dep.url); 137 + } 138 + } 139 + } 140 + 141 + // Public API 142 + export const LazyLoader = { 143 + // Direct loaders 144 + module: (url) => cachedLoad("module", url), 145 + script: (url) => cachedLoad("script", url), 146 + css: (url) => cachedLoad("css", url), 147 + json: (url) => cachedLoad("json", url), 148 + text: (url) => cachedLoad("text", url), 149 + 150 + // Unified load API - single descriptor or array for parallel 151 + load: (descriptorOrArray) => { 152 + if (Array.isArray(descriptorOrArray)) { 153 + return Promise.all(descriptorOrArray.map(loadDescriptor)); 154 + } 155 + // Return a chain for potential .then() calls 156 + return new LoaderChain().load(descriptorOrArray); 157 + }, 158 + 159 + // Manifest API 160 + fromManifest: (manifest) => new ManifestLoader(manifest), 161 + 162 + // Clear cache (for testing/hot reload) 163 + clearCache: () => cache.clear(), 164 + };
+1
ui/shared/search/utils.js
··· 26 26 if ( 27 27 !url.startsWith("about:") && 28 28 !url.startsWith("at:") && 29 + !url.startsWith("tile:") && 29 30 !url.startsWith("data:") && 30 31 !url.startsWith("file://") && 31 32 !url.startsWith("http://") &&