Something went wrong. Try again.
A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209use std::collections::HashMap;use std::future::Future;use std::pin::Pin;use std::sync::Arc;use wasmtime::{AsContextMut, Linker, Memory, TypedFunc};
use crate::plugin::caller::CallerSession;use crate::plugin::capabilities::{PluginCapability, is_free_import, requirement_for_import};
/// State stored in wasmtime's Store during plugin executionpub struct PluginState { pub plugin_id: String, pub scope: String, pub secrets: HashMap<String, String>, pub config: serde_json::Value, pub db: Option<sqlx::AnyPool>, pub db_backend: crate::db::DatabaseBackend, pub http_client: reqwest::Client, pub lexicons: Arc<crate::catalog::LexiconCatalog>, pub usage: super::ResourceUsage, pub memory: Option<Memory>, pub alloc: Option<TypedFunc<u32, u32>>, pub dealloc: Option<TypedFunc<(u32, u32), ()>>, pub capabilities: std::collections::HashSet<crate::plugin::capabilities::PluginCapability>, pub allowed_hosts: Vec<String>, pub plugin_type: crate::plugin::PluginType, pub executor: Option<crate::plugin::PluginExecutor>, pub call_ctx: crate::plugin::library::LibraryCallContext, pub depth: u8, /// The credentials of the user whose script is running, when there are /// any. Every `host_caller_*` import refuses without it, except /// `host_caller_xrpc_query`, which a session-less query, record-event or /// label script can also reach. pub caller: Option<Arc<crate::plugin::caller::CallerSession>>, /// The script run this instance is executing, for the five imports that /// act on it. `None` for a library or auth instance, which is what makes /// those imports refuse rather than act on a run they are not inside. pub script_run: Option<super::script::ScriptRun>, /// The full instance, carried from `PluginExecutor::app_state` so a /// caller-acting import can run without a `CallerSession` to source it /// from. `Some` for every real instantiation; `None` only for the /// direct-construction test and external-auth call sites that never /// reach such an import. pub app_state: Option<crate::AppState>, /// Answers the preview-1 imports of a module that links them. Built for /// every instance, because the linker only reaches it through the store /// and a module that imports nothing from it never calls in. pub wasi: wasmtime_wasi::p1::WasiP1Ctx, /// The guest-execution budget the store's epoch deadline is armed from. pub deadline: crate::plugin::runtime::GuestDeadline, /// The store's linear-memory ceiling, installed as its resource limiter. pub limiter: crate::plugin::runtime::MemoryLimiter, /// Library calls started through `host_call_library_start` and not yet /// waited on. Dropped with the store, which aborts whatever is left. pub pending_calls: super::PendingCalls, /// The panic a guest reported before trapping, which the script runner /// answers the run with instead of the trap. pub panic: Option<happyview_plugin_sdk::wire::PanicReport>, /// The fuel each call into this instance starts with. The plugin type's /// default, except a compiled module's, which is its run's budget. pub call_fuel: u64,}
/// Check that a memory access is within boundsfn check_bounds(offset: usize, length: usize, mem_size: usize) -> Result<(usize, usize), ()> { if length == 0 { return Ok((offset, offset)); } let end = offset.checked_add(length).ok_or(())?; if end > mem_size { return Err(()); } Ok((offset, end))}
/// Every host function except `host_log` starts here.pub(super) fn require_capability( state: &PluginState, req: crate::plugin::capabilities::Requirement,) -> Result<(), Vec<u8>> { if req.any_of.iter().any(|c| state.capabilities.contains(c)) { return Ok(()); } let wanted: Vec<&str> = req.any_of.iter().map(|c| c.as_str()).collect(); Err(error_envelope( "FORBIDDEN", format!( "plugin '{}' lacks the {} capability", state.plugin_id, wanted.join(" or ") ), ))}
fn error_envelope(code: &str, message: impl std::fmt::Display) -> Vec<u8> { serde_json::to_vec(&serde_json::json!({ "error": {"code": code, "message": message.to_string(), "retryable": false} })) .unwrap_or_default()}
/// Register all host functions with the linkerpub fn register_host_functions(linker: &mut Linker<PluginState>) -> Result<(), wasmtime::Error> { define_host_functions(linker).map(|_| ())}
/// Registers every async import through [`HostImports::define`], so none/// can be added without the guest-deadline accounting, and returns the/// names that went through it. `host_log` and `host_script_panic` are/// synchronous and copy a few bytes; they are registered bare.pub(crate) fn define_host_functions( linker: &mut Linker<PluginState>,) -> Result<Vec<&'static str>, wasmtime::Error> { // Sync functions linker.func_wrap("env", "host_log", host_log)?; linker.func_wrap("env", "host_script_panic", host_script_panic)?; let mut imports = HostImports::new(linker);
// host_get_secret must be async because it calls alloc on an async store imports.define( "host_get_secret", |caller, (name_ptr, name_len): (i32, i32)| { Box::pin(async move { host_get_secret_impl(caller, name_ptr, name_len).await }) }, )?;
// Async functions - HTTP imports.define( "host_http_request", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_http_request_impl(caller, req_ptr, req_len).await }) }, )?;
// Async functions - KV imports.define("host_kv_get", |caller, (key_ptr, key_len): (i32, i32)| { Box::pin(async move { host_kv_get_impl(caller, key_ptr, key_len).await }) })?;
imports.define( "host_kv_set", |caller, (key_ptr, key_len, val_ptr, val_len, ttl): (i32, i32, i32, i32, i32)| { Box::pin(async move { host_kv_set_impl(caller, key_ptr, key_len, val_ptr, val_len, ttl).await }) }, )?;
imports.define( "host_kv_delete", |caller, (key_ptr, key_len): (i32, i32)| { Box::pin(async move { host_kv_delete_impl(caller, key_ptr, key_len).await }) }, )?;
// Async functions - Record lookup imports.define( "host_lookup_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_lookup_record_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_records_query", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_records_query", req_ptr, req_len, |db, backend, spec| async move { super::records_query(&db, backend, spec).await }, ) .await }) }, )?;
imports.define( "host_records_count", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_records_count", req_ptr, req_len, |db, backend, spec| async move { super::records_count(&db, backend, spec).await }, ) .await }) }, )?;
imports.define( "host_records_get", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_records_get", req_ptr, req_len, |db, backend, spec: GetSpec| async move { super::records_get(&db, backend, &spec.uri).await }, ) .await }) }, )?;
imports.define( "host_records_search", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_records_search", req_ptr, req_len, |db, backend, spec| async move { super::records_search(&db, backend, spec).await }, ) .await }) }, )?;
imports.define( "host_table_query", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_table_query", req_ptr, req_len, |db, backend, spec| async move { super::table_query(&db, backend, spec).await }, ) .await }) }, )?;
imports.define( "host_backlinks_query", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_spec_impl( caller, "host_backlinks_query", req_ptr, req_len, |db, backend, spec| async move { super::backlinks_query(&db, backend, spec).await }, ) .await }) }, )?;
// Async functions - acting as the calling user imports.define( "host_caller_create_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_impl( caller, "host_caller_create_record", req_ptr, req_len, |session, spec| async move { super::create_record(&session, spec).await }, ) .await }) }, )?;
imports.define( "host_caller_put_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_impl( caller, "host_caller_put_record", req_ptr, req_len, |session, spec| async move { super::put_record(&session, spec).await }, ) .await }) }, )?;
imports.define( "host_caller_delete_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_impl( caller, "host_caller_delete_record", req_ptr, req_len, |session, spec| async move { super::delete_record(&session, spec).await }, ) .await }) }, )?;
imports.define( "host_caller_upload_blob", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_impl( caller, "host_caller_upload_blob", req_ptr, req_len, |session, spec| async move { super::upload_blob(&session, spec).await }, ) .await }) }, )?;
imports.define( "host_caller_xrpc_query", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_query_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_caller_xrpc_procedure", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_caller_impl( caller, "host_caller_xrpc_procedure", req_ptr, req_len, |session, spec| async move { super::xrpc_procedure(&session, spec).await }, ) .await }) }, )?;
// Async functions - writing to a linked repo and enqueuing jobs imports.define( "host_linked_repos_list", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repos_list", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, _spec: serde_json::Value| async move { super::linked_repos::list(&state).await }, ) .await }) }, )?;
imports.define( "host_linked_repo_create_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repo_create_record", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, spec: happyview_plugin_sdk::wire::LinkedRepoRecordCreate| async move { super::linked_repos::create_record(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_linked_repo_put_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repo_put_record", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, spec: happyview_plugin_sdk::wire::LinkedRepoRecordPut| async move { super::linked_repos::put_record(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_linked_repo_delete_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repo_delete_record", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, spec: happyview_plugin_sdk::wire::LinkedRepoRecordDelete| async move { super::linked_repos::delete_record(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_linked_repo_upload_blob", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repo_upload_blob", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, spec: happyview_plugin_sdk::wire::LinkedRepoBlobUpload| async move { super::linked_repos::upload_blob(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_linked_repo_call", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_linked_repo_call", req_ptr, req_len, super::linked_repos::LinkedRepoError::code, |state, spec: happyview_plugin_sdk::wire::LinkedRepoCall| async move { super::linked_repos::call(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_jobs_create", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_jobs_create", req_ptr, req_len, super::jobs::JobsError::code, |state, caller_did, session, spec: happyview_plugin_sdk::wire::JobCreate| async move { super::jobs::create(&state, caller_did.as_deref(), session.as_deref(), spec) .await }, ) .await }) }, )?;
// Reads of background jobs. `host_jobs_get` takes the caller DID from the // call context (a plugin cannot set it) and hides other users' jobs; the // `_any` pair need no caller, only the `jobs:read_any` capability. imports.define("host_jobs_get", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_jobs_get", req_ptr, req_len, super::jobs::JobsError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::JobGet| async move { super::jobs::get(&state, caller_did.as_deref(), spec).await }, ) .await }) })?;
imports.define( "host_jobs_get_any", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_jobs_get_any", req_ptr, req_len, super::jobs::JobsError::code, |state, spec: happyview_plugin_sdk::wire::JobGet| async move { super::jobs::get_any(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_jobs_list_any", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_jobs_list_any", req_ptr, req_len, super::jobs::JobsError::code, |state, spec: happyview_plugin_sdk::wire::JobListAny| async move { super::jobs::list_any(&state, spec).await }, ) .await }) }, )?;
// Async functions - spaces. Four reads need no caller; the eleven writes // act as `ctx.caller_did` and refuse before reaching the module when the // script context has none, via `require_spaces_caller`. imports.define( "host_spaces_info", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_spaces_info", req_ptr, req_len, super::spaces::SpacesError::code, |state, spec: happyview_plugin_sdk::wire::SpacesInfo| async move { super::spaces::info(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_query", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_spaces_query", req_ptr, req_len, super::spaces::SpacesError::code, |state, spec: happyview_plugin_sdk::wire::SpacesQuery| async move { super::spaces::query(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_members", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_spaces_members", req_ptr, req_len, super::spaces::SpacesError::code, |state, spec: happyview_plugin_sdk::wire::SpacesMembers| async move { super::spaces::members(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_access", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_impl( caller, "host_spaces_access", req_ptr, req_len, super::spaces::SpacesError::code, |state, spec: happyview_plugin_sdk::wire::SpacesAccess| async move { super::spaces::access(&state, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_create", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_create", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpacesCreate| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::create(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_accept_invite", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_accept_invite", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpacesAcceptInvite| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::accept_invite(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_write_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_write_record", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceRecordWrite| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::write_record(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_put_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_put_record", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceRecordPut| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::put_record(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_delete_record", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_delete_record", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceRecordDelete| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::delete_record(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_add_member", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_add_member", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceMemberAdd| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::add_member(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_set_member", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_set_member", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceMemberAdd| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::set_member(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_remove_member", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_remove_member", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceMemberRemove| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::remove_member(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_update", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_update", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceUpdate| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::update(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_delete", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_delete", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceDelete| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::delete(&state, &caller_did, spec).await }, ) .await }) }, )?;
imports.define( "host_spaces_create_invite", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_app_caller_impl( caller, "host_spaces_create_invite", req_ptr, req_len, super::spaces::SpacesError::code, |state, caller_did, _session, spec: happyview_plugin_sdk::wire::SpaceInviteCreate| async move { let caller_did = require_spaces_caller(caller_did)?; super::spaces::create_invite(&state, &caller_did, spec).await }, ) .await }) }, )?;
// Async functions - local index writes and lexicon reads imports.define( "host_records_index_put", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { // Plugin unit tests build no app state: their own pool then. let shared = shared_records(caller.data()); host_spec_impl( caller, "host_records_index_put", req_ptr, req_len, |db, backend, spec| async move { let store = shared .unwrap_or_else(|| crate::record_store::RecordStore::new(db, backend)); super::index_put(&store, spec).await }, ) .await }) }, )?;
imports.define( "host_records_index_delete", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { // Plugin unit tests build no app state: their own pool then. let shared = shared_records(caller.data()); host_spec_impl( caller, "host_records_index_delete", req_ptr, req_len, |db, backend, spec: happyview_plugin_sdk::wire::IndexDelete| async move { let store = shared .unwrap_or_else(|| crate::record_store::RecordStore::new(db, backend)); super::index_delete(&store, &spec.uri).await }, ) .await }) }, )?;
imports.define( "host_lexicon_get", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_lexicon_get_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_allowed_hosts", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_allowed_hosts_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_call_library", |caller, (lib_ptr, lib_len, fn_ptr, fn_len, args_ptr, args_len): (i32, i32, i32, i32, i32, i32)| { Box::pin(async move { host_call_library_impl(caller, lib_ptr, lib_len, fn_ptr, fn_len, args_ptr, args_len) .await }) }, )?;
imports.define( "host_call_library_start", |caller, (lib_ptr, lib_len, fn_ptr, fn_len, args_ptr, args_len): (i32, i32, i32, i32, i32, i32)| { Box::pin(async move { host_call_library_start_impl( caller, lib_ptr, lib_len, fn_ptr, fn_len, args_ptr, args_len, ) }) }, )?;
imports.define( "host_call_library_wait_any", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_call_library_wait_any_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_get_api_surface", |caller, (lib_ptr, lib_len): (i32, i32)| { Box::pin(async move { host_get_api_surface_impl(caller, lib_ptr, lib_len).await }) }, )?;
imports.define( "host_db_query", |caller, (sql_ptr, sql_len, params_ptr, params_len): (i32, i32, i32, i32)| { Box::pin(async move { host_db_impl(caller, false, sql_ptr, sql_len, params_ptr, params_len).await }) }, )?; imports.define( "host_db_execute", |caller, (sql_ptr, sql_len, params_ptr, params_len): (i32, i32, i32, i32)| { Box::pin(async move { host_db_impl(caller, true, sql_ptr, sql_len, params_ptr, params_len).await }) }, )?;
// Async functions - AT Protocol network reads and attestation imports.define("host_blob_put", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_blob_put_impl(caller, req_ptr, req_len).await }) })?;
imports.define("host_blob_get", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_blob_get_impl(caller, req_ptr, req_len).await }) })?;
imports.define( "host_blob_stat", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_blob_stat_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_atproto_resolve_service", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin( async move { host_atproto_resolve_service_impl(caller, req_ptr, req_len).await }, ) }, )?;
imports.define( "host_atproto_resolve_identity", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin( async move { host_atproto_resolve_identity_impl(caller, req_ptr, req_len).await }, ) }, )?;
imports.define( "host_atproto_blob_download", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_atproto_blob_download_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_labels_get", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_labels_get_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_attest_sign", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_attest_sign_impl(caller, req_ptr, req_len).await }) }, )?;
imports.define( "host_attest_verify", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_attest_verify_impl(caller, req_ptr, req_len).await }) }, )?;
// The imports only an interpreter reaches: they act on the script run in // progress, so each refuses outside one. imports.define( "host_script_log", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_script_impl( caller, "host_script_log", req_ptr, req_len, |state, run, spec: happyview_plugin_sdk::wire::ScriptLogRequest| async move { super::script::log(&state, &run, spec).await }, ) .await }) }, )?;
imports.define( "host_job_progress", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_script_impl( caller, "host_job_progress", req_ptr, req_len, |state, run, spec: happyview_plugin_sdk::wire::JobProgressRequest| async move { super::script::progress(&state, &run, spec).await }, ) .await }) }, )?;
imports.define( "host_job_should_stop", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_script_impl( caller, "host_job_should_stop", req_ptr, req_len, |state, run, _spec: happyview_plugin_sdk::wire::JobShouldStopRequest| async move { super::script::should_stop(&state, &run).await }, ) .await }) }, )?;
imports.define("host_job_wait", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_script_impl( caller, "host_job_wait", req_ptr, req_len, |state, run, spec: happyview_plugin_sdk::wire::JobWaitRequest| async move { super::script::wait(&state, &run, spec).await }, ) .await }) })?;
imports.define( "host_module_resolve", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_script_impl( caller, "host_module_resolve", req_ptr, req_len, |_state, run, spec: happyview_plugin_sdk::wire::ModuleResolve| async move { super::script::module_resolve(&run, spec) }, ) .await }) }, )?;
// The test runner's imports. Each answers only inside a test file's run // on a state the test command built. imports.define( "host_test_report", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_test_impl( caller, "host_test_report", req_ptr, req_len, |_state, harness, report: crate::plugin::TestReport| async move { harness.report(report); Ok(()) }, ) .await }) }, )?;
imports.define( "host_test_trigger", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_test_impl( caller, "host_test_trigger", req_ptr, req_len, |state, harness, spec: crate::plugin::TestTrigger| async move { crate::testing::triggers::fire(&state, &harness, spec).await }, ) .await }) }, )?;
imports.define( "host_test_mock", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_test_impl( caller, "host_test_mock", req_ptr, req_len, |state, harness, spec: crate::plugin::TestMock| async move { crate::testing::mocks::mock(&state, &harness, spec).await }, ) .await }) }, )?;
imports.define( "host_test_calls", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_test_impl( caller, "host_test_calls", req_ptr, req_len, |_state, harness, spec: crate::plugin::TestCalls| async move { Ok(harness.calls(&spec.namespace)) }, ) .await }) }, )?;
imports.define( "host_test_pds_writes", |caller, (req_ptr, req_len): (i32, i32)| { Box::pin(async move { host_test_impl( caller, "host_test_pds_writes", req_ptr, req_len, |_state, harness, _spec: crate::plugin::TestPdsWritesRequest| async move { Ok(harness.pds().writes()) }, ) .await }) }, )?;
Ok(imports.wrapped)}
pub(crate) struct HostImports<'l> { linker: &'l mut Linker<PluginState>, wrapped: Vec<&'static str>,}
impl<'l> HostImports<'l> { pub(crate) fn new(linker: &'l mut Linker<PluginState>) -> Self { Self { linker, wrapped: Vec::new(), } }
/// One `env` import. The guest's deadline stops when it enters and /// starts again when it returns, so an import's own latency is never /// charged to the script. pub(crate) fn define<Params, R, F>( &mut self, name: &'static str, f: F, ) -> Result<(), wasmtime::Error> where Params: wasmtime::WasmTyList + Send + 'static, R: wasmtime::WasmRet + Send + 'static, F: for<'a, 'c> Fn( &'a mut wasmtime::Caller<'c, PluginState>, Params, ) -> Pin<Box<dyn Future<Output = R> + Send + 'a>> + Copy + Send + Sync + 'static, { self.wrapped.push(name); self.linker.func_wrap_async( "env", name, move |mut caller: wasmtime::Caller<'_, PluginState>, params: Params| { Box::new(async move { suspend_guest_deadline(&mut caller); let out = f(&mut caller, params).await; resume_guest_deadline(&mut caller); out }) }, )?; Ok(()) }}
/// The guest has entered a host import: stop its clock.fn suspend_guest_deadline(caller: &mut wasmtime::Caller<'_, PluginState>) { caller.data_mut().deadline.suspend();}
/// The guest is about to run again: re-arm the store with what is left.fn resume_guest_deadline(caller: &mut wasmtime::Caller<'_, PluginState>) { let remaining = caller.data_mut().deadline.resume(); caller.as_context_mut().set_epoch_deadline(remaining);}
/// One entry point for the five imports that act on the script run in/// progress. The capability is checked before the run, so a plugin that never/// asked for `script:host` hears about the grant rather than about a run it/// could not have reached anyway.async fn host_script_impl<S, R, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, run: F,) -> i64where S: serde::de::DeserializeOwned, R: serde::Serialize, F: FnOnce(crate::AppState, super::script::ScriptRun, S) -> Fut, Fut: std::future::Future<Output = Result<R, super::script::ScriptHostError>>,{ if let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(script_run) = caller.data().script_run.clone() else { return write_guest_response( caller, &error_envelope( "UNSUPPORTED", format!("{import} acts on a script run, and no script run is in progress"), ), ) .await; }; let Some(app_state) = caller.data().app_state.clone() else { return write_guest_response( caller, &error_envelope("HOST_ERROR", "this instance has no app state"), ) .await; }; let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: S = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let response = match run(app_state, script_run, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(e.code(), e), }; write_guest_response(caller, &response).await}
/// One entry point for the test runner's imports. Both the run and the state/// have to say "test": a test-kind input reaching a serving instance's state,/// or a handler's run on a test state, is refused alike.async fn host_test_impl<S, R, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, run: F,) -> i64where S: serde::de::DeserializeOwned, R: serde::Serialize, F: FnOnce(crate::AppState, Arc<crate::testing::TestHarness>, S) -> Fut, Fut: std::future::Future<Output = Result<R, crate::testing::TestHostError>>,{ if let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return write_guest_response(caller, &envelope).await; } let in_test_file = caller .data() .script_run .as_ref() .is_some_and(|run| run.test); let app_state = caller.data().app_state.clone(); let harness = app_state.as_ref().and_then(|state| state.test.clone()); let (true, Some(app_state), Some(harness)) = (in_test_file, app_state, harness) else { return write_guest_response( caller, &error_envelope( "UNSUPPORTED", format!("{import} is available only to a test file run by `happyview test`"), ), ) .await; }; let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: S = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let response = match run(app_state, harness, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(e.code(), e), }; write_guest_response(caller, &response).await}
/// Read a string from guest memoryfn read_guest_string( caller: &wasmtime::Caller<'_, PluginState>, ptr: i32, len: i32,) -> Option<String> { let memory = caller.data().memory?; let mem_data = memory.data(caller); let (start, end) = check_bounds(ptr as usize, len as usize, mem_data.len()).ok()?; std::str::from_utf8(&mem_data[start..end]) .ok() .map(String::from)}
/// Read raw bytes from guest memoryfn read_guest_bytes( caller: &wasmtime::Caller<'_, PluginState>, ptr: i32, len: i32,) -> Option<Vec<u8>> { let memory = caller.data().memory?; let mem_data = memory.data(caller); let (start, end) = check_bounds(ptr as usize, len as usize, mem_data.len()).ok()?; Some(mem_data[start..end].to_vec())}
/// Write response data to guest memory, returning packed (ptr << 32) | lenasync fn write_guest_response(caller: &mut wasmtime::Caller<'_, PluginState>, data: &[u8]) -> i64 { let memory = match caller.data().memory { Some(m) => m, None => return 0, }; let alloc = match &caller.data().alloc { Some(a) => a.clone(), None => return 0, };
// The allocator is guest code: the clock has to be running again, and // armed from what remains, before it is entered. resume_guest_deadline(caller); crate::plugin::runtime::grant_fuel_for_bytes(caller, data.len()); let len = data.len() as u32; let ptr = match alloc.call_async(&mut *caller, len).await { Ok(p) if p != 0 => p, _ => return 0, };
let mem_data = memory.data_mut(caller); if check_bounds(ptr as usize, len as usize, mem_data.len()).is_err() { return 0; }
mem_data[ptr as usize..(ptr as usize + len as usize)].copy_from_slice(data); ((ptr as i64) << 32) | (len as i64)}
/// Host function: log a message from the pluginfn host_log( caller: wasmtime::Caller<'_, PluginState>, level_ptr: i32, level_len: i32, msg_ptr: i32, msg_len: i32,) { let memory = match caller.data().memory { Some(m) => m, None => return, };
let mem_data = memory.data(&caller); let mem_size = mem_data.len();
let (level_start, level_end) = match check_bounds(level_ptr as usize, level_len as usize, mem_size) { Ok(bounds) => bounds, Err(_) => return, };
let (msg_start, msg_end) = match check_bounds(msg_ptr as usize, msg_len as usize, mem_size) { Ok(bounds) => bounds, Err(_) => return, };
let level = std::str::from_utf8(&mem_data[level_start..level_end]).unwrap_or("info"); let msg = std::str::from_utf8(&mem_data[msg_start..msg_end]).unwrap_or("");
let plugin_id = caller.data().plugin_id.clone(); let db = caller.data().db.clone(); let db_backend = caller.data().db_backend; let log_level: super::LogLevel = level.parse().unwrap_or_default(); super::log(&plugin_id, log_level, msg, db, db_backend);}
/// The longest panic report the host reads. The message reaches an API/// caller's error body and the logs, so its size is not the guest's to choose.pub(crate) const MAX_PANIC_REPORT_BYTES: usize = 64 * 1024;
/// Host function: record the panic a guest is about to trap on. A report/// that cannot be read, or is longer than [`MAX_PANIC_REPORT_BYTES`], is/// dropped unparsed, and the run then ends as the plain trap it would have/// been anyway.fn host_script_panic(mut caller: wasmtime::Caller<'_, PluginState>, ptr: i32, len: i32) { if len as u32 as usize > MAX_PANIC_REPORT_BYTES { return; } let Some(memory) = caller.data().memory else { return; }; let data = memory.data(&caller); let Ok((start, end)) = check_bounds(ptr as usize, len as usize, data.len()) else { return; }; if let Ok(report) = serde_json::from_slice(&data[start..end]) { caller.data_mut().panic = Some(report); }}
/// Host function: get a secret value by name/// Returns a packed i64: (ptr << 32) | len, or 0 on error////// The response is JSON-encoded as `{"ok": "<value>"}` so plugins can/// deserialize it with the same `Response<String>` envelope they use for/// other host calls.async fn host_get_secret_impl( caller: &mut wasmtime::Caller<'_, PluginState>, name_ptr: i32, name_len: i32,) -> i64 { if let Err(envelope) = require_capability( caller.data(), requirement_for_import("host_get_secret").unwrap(), ) { return write_guest_response(caller, &envelope).await; }
let name = match read_guest_string(caller, name_ptr, name_len) { Some(n) => n, None => return 0, };
let value = match caller.data().secrets.get(&name) { Some(v) => v.clone(), None => return 0, };
let response_bytes = serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(); write_guest_response(caller, &response_bytes).await}
// ============================================================================// Async host function implementations// ============================================================================
/// Build a HostContext from PluginState, requires db to be presentfn build_host_context(state: &PluginState) -> Option<super::HostContext> { let db = state.db.clone()?; Some(super::HostContext { plugin_id: state.plugin_id.clone(), scope: state.scope.clone(), secrets: state.secrets.clone(), config: state.config.clone(), records: records_or_local(state, &db), db, db_backend: state.db_backend, http_client: state.http_client.clone(), lexicons: state.lexicons.clone(), })}
/// Host function: make an HTTP requestasync fn host_http_request_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { if let Err(envelope) = require_capability( caller.data(), requirement_for_import("host_http_request").unwrap(), ) { return write_guest_response(caller, &envelope).await; }
let req_bytes = match read_guest_bytes(caller, req_ptr, req_len) { Some(b) => b, None => { tracing::error!( "host_http_request: failed to read guest memory (ptr={req_ptr}, len={req_len})" ); return 0; } };
let request: super::HttpRequest = match serde_json::from_slice(&req_bytes) { Ok(r) => r, Err(e) => { tracing::error!("host_http_request: failed to parse request JSON: {e}"); return 0; } };
let url = request.url.clone(); let method = request.method.clone();
let unrestricted = { let state = caller.data(); let unrestricted = state .capabilities .contains(&PluginCapability::NetworkRequestUnrestricted); if !unrestricted { let host = reqwest::Url::parse(&request.url) .ok() .and_then(|u| u.host_str().map(String::from)); let ok = host .as_deref() .map(|h| super::host_allowed(&state.allowed_hosts, h)) .unwrap_or(false); if !ok { let defined = state .capabilities .contains(&PluginCapability::NetworkRequestDefined); let message = super::allowed_hosts_denial(&state.allowed_hosts, defined, host.as_deref()); return write_guest_response(caller, &error_envelope("FORBIDDEN", message)).await; } } unrestricted };
let ctx = match build_host_context(caller.data()) { Some(c) => c, None => { tracing::error!("host_http_request: failed to build host context (db missing?)"); return 0; } };
let result = { let usage = &mut caller.data_mut().usage; super::http_request(&ctx, usage, request, unrestricted).await };
let response_bytes = match result { Ok(resp) => serde_json::to_vec(&serde_json::json!({"ok": resp})).unwrap_or_default(), Err(e) => { tracing::warn!("host_http_request: HTTP {method} {url} failed: {e}"); serde_json::to_vec(&serde_json::json!({ "error": {"code": "HTTP_ERROR", "message": e.to_string(), "retryable": false} })) .unwrap_or_default() } };
let packed = write_guest_response(caller, &response_bytes).await; if packed == 0 { tracing::error!( "host_http_request: write_guest_response returned 0 for {} {} (response_len={})", method, url, response_bytes.len() ); } packed}
/// Host function: get a value from KV storeasync fn host_kv_get_impl( caller: &mut wasmtime::Caller<'_, PluginState>, key_ptr: i32, key_len: i32,) -> i64 { if let Err(envelope) = require_capability( caller.data(), requirement_for_import("host_kv_get").unwrap(), ) { return write_guest_response(caller, &envelope).await; }
let key = match read_guest_string(caller, key_ptr, key_len) { Some(k) => k, None => return 0, };
let ctx = match build_host_context(caller.data()) { Some(c) => c, None => return 0, };
let result = super::kv_get(&ctx, &key).await;
let response_bytes = match result { Ok(Some(value)) => { serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default() } Ok(None) => return 0, Err(e) => serde_json::to_vec(&serde_json::json!({ "error": {"code": "KV_ERROR", "message": e.to_string(), "retryable": false} })) .unwrap_or_default(), };
write_guest_response(caller, &response_bytes).await}
/// Host function: set a value in KV store. Returns `0` on success and `-1`/// on any failure, including a missing `kv:write` capability — the `env`/// import signature is a bare `i32`, so there's no envelope to carry a/// distinct "forbidden" code back to the guest.async fn host_kv_set_impl( caller: &mut wasmtime::Caller<'_, PluginState>, key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl: i32,) -> i32 { if require_capability( caller.data(), requirement_for_import("host_kv_set").unwrap(), ) .is_err() { tracing::warn!( plugin_id = %caller.data().plugin_id, "host_kv_set: missing kv:write capability" ); return -1; }
let key = match read_guest_string(caller, key_ptr, key_len) { Some(k) => k, None => return -1, }; let value = match read_guest_bytes(caller, val_ptr, val_len) { Some(v) => v, None => return -1, };
let ttl_secs = if ttl > 0 { Some(ttl as u32) } else { None };
let ctx = match build_host_context(caller.data()) { Some(c) => c, None => return -1, };
let usage = &mut caller.data_mut().usage; match super::kv_set(&ctx, usage, &key, value, ttl_secs).await { Ok(()) => 0, // The import's return is a bare `i32`, so the reason cannot reach the // guest; without this it reaches nobody. Err(e) => { tracing::warn!(plugin_id = %caller.data().plugin_id, error = %e, "host_kv_set failed"); -1 } }}
/// Host function: delete a value from KV store. Returns `0` on success and/// `-1` on any failure, including a missing `kv:write` capability — the/// `env` import signature is a bare `i32`, so there's no envelope to carry a/// distinct "forbidden" code back to the guest.async fn host_kv_delete_impl( caller: &mut wasmtime::Caller<'_, PluginState>, key_ptr: i32, key_len: i32,) -> i32 { if require_capability( caller.data(), requirement_for_import("host_kv_delete").unwrap(), ) .is_err() { tracing::warn!( plugin_id = %caller.data().plugin_id, "host_kv_delete: missing kv:write capability" ); return -1; }
let key = match read_guest_string(caller, key_ptr, key_len) { Some(k) => k, None => return -1, };
let ctx = match build_host_context(caller.data()) { Some(c) => c, None => return -1, };
match super::kv_delete(&ctx, &key).await { Ok(()) => 0, Err(_) => -1, }}
/// Host function: look up an AT Protocol recordasync fn host_lookup_record_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { if let Err(envelope) = require_capability( caller.data(), requirement_for_import("host_lookup_record").unwrap(), ) { return write_guest_response(caller, &envelope).await; }
let req_bytes = match read_guest_bytes(caller, req_ptr, req_len) { Some(b) => b, None => return 0, };
let request: super::LookupRequest = match serde_json::from_slice(&req_bytes) { Ok(r) => r, Err(_) => return 0, };
let ctx = match build_host_context(caller.data()) { Some(c) => c, None => return 0, };
let result = super::lookup_record_by_request(&ctx, request).await;
let response_bytes = match result { Ok(record) => serde_json::to_vec(&serde_json::json!({"ok": record})).unwrap_or_default(), Err(e) => serde_json::to_vec(&serde_json::json!({ "error": {"code": "LOOKUP_ERROR", "message": e.to_string(), "retryable": false} })) .unwrap_or_default(), };
write_guest_response(caller, &response_bytes).await}
/// `host_lexicon_get` reads the in-memory lexicon registry, not the database,/// so it can't go through `host_spec_impl` — that refuses instances with no/// pool, which a lexicon lookup never needed in the first place.async fn host_lexicon_get_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::LexiconGet = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let lexicons = caller.data().lexicons.clone(); let value = super::lexicon_get(&lexicons, &spec.nsid).await; let response = serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(); write_guest_response(caller, &response).await}
/// The plugin's effective allowed-hosts list, resolved once at instantiation/// and carried on `PluginState`: the operator's list under/// `network:request:defined`, the manifest's under `network:request`, or/// empty otherwise. A plugin reads this to behave sensibly no matter which/// of the three network capabilities it holds.fn resolved_allowed_hosts(state: &PluginState) -> Vec<String> { state.allowed_hosts.clone()}
/// `host_allowed_hosts` is a free import: the list itself carries nothing/// secret, and a plugin needs to be able to read it regardless of which/// capability (if any) it holds. The request is always `{}`; nothing to/// decode.async fn host_allowed_hosts_impl( caller: &mut wasmtime::Caller<'_, PluginState>, _req_ptr: i32, _req_len: i32,) -> i64 { let hosts = resolved_allowed_hosts(caller.data()); let response = serde_json::to_vec(&serde_json::json!({"ok": hosts})).unwrap_or_default(); write_guest_response(caller, &response).await}
/// The instance's shared record store, which carries the interned-id cache,/// the compression dictionaries and the external storage's routing; `None`/// for an instance built without the app state (plugin unit tests), which/// then reads and writes through a store of its own.fn shared_records(state: &PluginState) -> Option<crate::record_store::RecordStore> { state.app_state.as_ref().map(|app| app.records.clone())}
/// [`shared_records`], or, for an instance built without the app state/// (plugin unit tests), a local store over its own pool.fn records_or_local(state: &PluginState, db: &sqlx::AnyPool) -> crate::record_store::RecordStore { shared_records(state) .unwrap_or_else(|| crate::record_store::RecordStore::new(db.clone(), state.db_backend))}
/// The record collection a spec reads, which picks the database it runs on/// (an external storage's route when that claims the collection).trait SpecCollection { fn collection(&self) -> Option<&str>;
/// True when the spec reads the record views, which run where raw SQL /// reads of them do: on the external storage's raw read route, if any. fn reads_record_views(&self) -> bool { false }}
impl SpecCollection for happyview_plugin_sdk::wire::RecordsQuery { fn collection(&self) -> Option<&str> { Some(&self.collection) }}
impl SpecCollection for happyview_plugin_sdk::wire::RecordsCount { fn collection(&self) -> Option<&str> { Some(&self.collection) }}
impl SpecCollection for happyview_plugin_sdk::wire::RecordsSearch { fn collection(&self) -> Option<&str> { Some(&self.collection) }}
impl SpecCollection for happyview_plugin_sdk::wire::BacklinksQuery { fn collection(&self) -> Option<&str> { Some(&self.collection) }}
impl SpecCollection for GetSpec { fn collection(&self) -> Option<&str> { crate::record_store::codec::parse_record_uri(&self.uri).map(|(_, collection, _)| collection) }}
/// A table query reads the instance's own tables, or the record views.impl SpecCollection for happyview_plugin_sdk::wire::TableQuery { fn collection(&self) -> Option<&str> { None }
fn reads_record_views(&self) -> bool { crate::raw_sql_guard::is_record_view(&self.table) }}
/// Index writes route inside the shared store, not by pool; their collection/// says whose storage a failure belongs to.impl SpecCollection for happyview_plugin_sdk::wire::IndexPut { fn collection(&self) -> Option<&str> { Some(&self.collection) }}
impl SpecCollection for happyview_plugin_sdk::wire::IndexDelete { fn collection(&self) -> Option<&str> { crate::record_store::codec::parse_record_uri(&self.uri).map(|(_, collection, _)| collection) }}
/// One entry point for every spec-taking record/table query import: gate on/// the import's capability, decode the spec, run it against the database, and/// wrap the result in the standard envelope. `RecordsError::InvalidSpec`/// becomes `INVALID_SPEC` rather than `DB_ERROR` so a plugin can tell a bad/// query from a database failure.async fn host_spec_impl<S, R, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, run: F,) -> i64where S: serde::de::DeserializeOwned + SpecCollection, R: serde::Serialize, F: FnOnce(sqlx::AnyPool, crate::db::DatabaseBackend, S) -> Fut, Fut: std::future::Future<Output = Result<R, super::RecordsError>>,{ // A free import has no row in the requirements table and nothing to gate // on; anything else missing one is a programming error, not a grant. if !is_free_import(import) && let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: S = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(db) = caller.data().db.clone() else { return write_guest_response(caller, &error_envelope("HOST_ERROR", "no database")).await; }; let backend = caller.data().db_backend; // A claimed collection is read on the external storage's route, and the // record views on its raw read route; every other spec runs on the // instance's own database, as it always has. let external = caller.data().app_state.as_ref().and_then(|app| { if spec.reads_record_views() { let ext = app.records.external()?; return ext.raw_route().map(|route| (ext.clone(), route)); } let collection = spec.collection()?; app.records .external_route(collection) .map(|ext| (ext.clone(), ext.read_route(collection))) }); let (db, backend) = match &external { Some((_, route)) => (route.pool.clone(), route.backend), None => (db, backend), }; let response = match run(db, backend, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(super::RecordsError::InvalidSpec(msg)) => error_envelope("INVALID_SPEC", msg), // The claiming storage is down: a code of its own, so a script that // passes it on answers 503 rather than failing. Err(super::RecordsError::Database(e)) if external .as_ref() .is_some_and(|(ext, _)| ext.unavailable(&e)) => { error_envelope(crate::error::STORAGE_UNAVAILABLE, e) } Err(e) => error_envelope("DB_ERROR", e), }; write_guest_response(caller, &response).await}
/// One entry point for every import that acts as the calling user: gate on/// the import's capability, then on there being a user at all, then decode the/// spec and run it. The session comes off the instance rather than the spec,/// so a library cannot name a user it was not lent.async fn host_caller_impl<S, R, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, run: F,) -> i64where S: serde::de::DeserializeOwned, R: serde::Serialize, F: FnOnce(Arc<CallerSession>, S) -> Fut, Fut: std::future::Future<Output = Result<R, super::CallerError>>,{ if let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(session) = caller.data().caller.clone() else { return write_guest_response( caller, &error_envelope( "NO_SESSION", "this script context has no caller session, so nothing can be done as a user here", ), ) .await; }; let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: S = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let response = match run(session, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(e.code(), caller_error_message(&e)), }; write_guest_response(caller, &response).await}
/// The gate/decode steps [`host_app_impl`] and [`host_app_caller_impl`]/// share: capability, then an app state to act against at all — only the/// direct-construction test and external-auth call sites leave it unset —/// then the decoded spec. `Err` carries the return value a failure has/// already written, so both callers can return it directly.async fn host_app_gate<S>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32,) -> Result<(crate::AppState, S), i64>where S: serde::de::DeserializeOwned,{ if let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return Err(write_guest_response(caller, &envelope).await); } let Some(app_state) = caller.data().app_state.clone() else { return Err(write_guest_response( caller, &error_envelope("HOST_ERROR", "this instance has no app state"), ) .await); }; let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return Err(0); }; let spec: S = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return Err(write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await), }; Ok((app_state, spec))}
/// One entry point for the six linked-repo imports: each spec already names/// the DID whose linked repo it acts on, so none of them needs the runner's/// own identity — only the whole instance to act against.async fn host_app_impl<S, R, E, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, code: fn(&E) -> &'static str, run: F,) -> i64where S: serde::de::DeserializeOwned, R: serde::Serialize, E: std::fmt::Display, F: FnOnce(crate::AppState, S) -> Fut, Fut: std::future::Future<Output = Result<R, E>>,{ let (app_state, spec) = match host_app_gate(caller, import, req_ptr, req_len).await { Ok(pair) => pair, Err(early_return) => return early_return, }; let response = match run(app_state, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(code(&e), e), }; write_guest_response(caller, &response).await}
/// As [`host_app_impl`], but also threads the runner's own caller DID and/// (when it has one) its caller session through to `run` — what/// `host_jobs_create` needs to enqueue as the script's runner, rather than as/// whoever a spec field might name.async fn host_app_caller_impl<S, R, E, F, Fut>( caller: &mut wasmtime::Caller<'_, PluginState>, import: &'static str, req_ptr: i32, req_len: i32, code: fn(&E) -> &'static str, run: F,) -> i64where S: serde::de::DeserializeOwned, R: serde::Serialize, E: std::fmt::Display, F: FnOnce(crate::AppState, Option<String>, Option<Arc<CallerSession>>, S) -> Fut, Fut: std::future::Future<Output = Result<R, E>>,{ let (app_state, spec) = match host_app_gate(caller, import, req_ptr, req_len).await { Ok(pair) => pair, Err(early_return) => return early_return, }; let caller_did = caller.data().call_ctx.caller_did.clone(); let session = caller.data().caller.clone(); let response = match run(app_state, caller_did, session, spec).await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(code(&e), e), }; write_guest_response(caller, &response).await}
/// Every space write acts as `ctx.caller_did` and has no other identity to/// fall back to. Refusing here, before the module runs, keeps that rule in/// one place rather than in each of the eleven write functions.fn require_spaces_caller(caller_did: Option<String>) -> Result<String, super::spaces::SpacesError> { caller_did.ok_or_else(|| { super::spaces::SpacesError::BadInput( "a space write needs a caller; this script context has none".into(), ) })}
/// `host_caller_xrpc_query` on its own: gate on the capability, decode the/// spec, and run it, but — unlike [`host_caller_impl`] — never refuse for/// lacking a session. A query needs no PDS auth to run, and a query,/// record-event or label script has no session to lend. The session, when the/// runner has one, still supplies its claims; otherwise the call context's/// `caller_did` stands in.async fn host_caller_query_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_caller_xrpc_query"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::CallerXrpcQuery = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(app_state) = caller.data().app_state.clone() else { return write_guest_response( caller, &error_envelope( "HOST_ERROR", "this instance has no app state to query against", ), ) .await; }; let session = caller.data().caller.clone(); let caller_did = caller.data().call_ctx.caller_did.clone(); let response = match super::xrpc_query( &app_state, session.as_deref(), caller_did.as_deref(), spec, ) .await { Ok(value) => serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default(), Err(e) => error_envelope(e.code(), caller_error_message(&e)), }; write_guest_response(caller, &response).await}
/// A dead session has to survive the trip out through the guest and the Lua/// bridge, which flatten everything into one error string. The prefix is what/// the script executor recognises there to answer 401 rather than 500.fn caller_error_message(e: &super::CallerError) -> String { match e { super::CallerError::Auth(_) => { format!("{}{e}", crate::error::LUA_AUTH_ERROR_PREFIX) } other => other.to_string(), }}
/// The host-side input to `host_records_get`: the SDK wrapper sends/// `{"uri": ...}` rather than a bare string so it shares `host_spec_impl`'s/// JSON-spec envelope with every other record/table import.#[derive(serde::Deserialize)]struct GetSpec { uri: String,}
/// Gate on `library:call`, then hand back the executor.fn library_access( state: &PluginState, import: &str,) -> Result<crate::plugin::PluginExecutor, Vec<u8>> { require_capability( state, crate::plugin::capabilities::requirement_for_import(import).unwrap(), )?; state .executor .clone() .ok_or_else(|| error_envelope("HOST_ERROR", "no executor attached to this instance"))}
async fn host_call_library_impl( caller: &mut wasmtime::Caller<'_, PluginState>, lib_ptr: i32, lib_len: i32, fn_ptr: i32, fn_len: i32, args_ptr: i32, args_len: i32,) -> i64 { let (Some(lib), Some(function), Some(args_bytes)) = ( read_guest_string(caller, lib_ptr, lib_len), read_guest_string(caller, fn_ptr, fn_len), read_guest_bytes(caller, args_ptr, args_len), ) else { return 0; }; let outcome = match library_call( caller.data(), "host_call_library", lib, function, &args_bytes, ) { Ok(call) => call.await, Err(refused) => refused, }; write_guest_response(caller, &serde_json::to_vec(&outcome).unwrap_or_default()).await}
/// One library call, checked and ready to run: everything it needs is owned,/// so `host_call_library` can await it in place and/// `host_call_library_start` can hand it to a task of its own. It runs one/// hop deeper than `state`, as `state`'s caller, under `state`'s context./// Anything wrong with the request is the `Err`, already an envelope.fn library_call( state: &PluginState, import: &str, lib: String, function: String, args_bytes: &[u8],) -> Result< impl Future<Output = super::pending_calls::CallOutcome> + Send + 'static, super::pending_calls::CallOutcome,> { let refused = |envelope: Vec<u8>| { serde_json::from_slice(&envelope) .unwrap_or_else(|_| call_error("HOST_ERROR", "unreadable refusal")) }; let args: Vec<serde_json::Value> = match serde_json::from_slice(args_bytes) { Ok(serde_json::Value::Array(a)) => a, Ok(_) => return Err(call_error("BAD_INPUT", "args must be a JSON array")), Err(e) => return Err(call_error("BAD_INPUT", e)), }; let executor = library_access(state, import).map_err(refused)?; let ctx = state.call_ctx.clone(); let depth = state.depth + 1; // A library reached through another library acts as the same user, so the // session travels the whole chain rather than stopping at the first hop. let session = state.caller.clone(); Ok(async move { match executor .call_library_as(&lib, &function, &args, &ctx, session, depth) .await { Ok(ok) => super::pending_calls::CallOutcome::Ok { ok }, Err(crate::plugin::ExecutionError::PluginError { code, message, retryable, }) => super::pending_calls::CallOutcome::Err { error: crate::plugin::PluginEnvelopeError { code, message, retryable, }, }, Err(e) => call_error("LIBRARY_ERROR", e), } })}
/// [`error_envelope`], as the typed outcome a started call settles with.fn call_error(code: &str, message: impl std::fmt::Display) -> super::pending_calls::CallOutcome { super::pending_calls::CallOutcome::Err { error: crate::plugin::PluginEnvelopeError::new(code, message.to_string()), }}
/// Start a library call and hand back its handle without waiting for it. The/// guest is never suspended on the call's work, and the only failure answered/// here is guest memory that cannot be read (`0`, as `host_call_library`/// answers it); every other one settles through the handle, so an interpreter/// reads every failure of a call from one place.fn host_call_library_start_impl( caller: &mut wasmtime::Caller<'_, PluginState>, lib_ptr: i32, lib_len: i32, fn_ptr: i32, fn_len: i32, args_ptr: i32, args_len: i32,) -> i32 { let (Some(lib), Some(function), Some(args_bytes)) = ( read_guest_string(caller, lib_ptr, lib_len), read_guest_string(caller, fn_ptr, fn_len), read_guest_bytes(caller, args_ptr, args_len), ) else { return 0; }; let call = library_call( caller.data(), "host_call_library_start", lib, function, &args_bytes, ); let pending = &mut caller.data_mut().pending_calls; let handle = match call { Ok(call) => pending.start(call), Err(refused) => pending.settled(refused), }; handle.map_or(0, |h| h as i32)}
/// Suspend the guest until one of the listed calls settles and answer its/// envelope with its handle beside it. A request that could never be answered/// — malformed, empty, or naming a handle that is not outstanding — is refused/// at once with no `handle`, rather than left to hang.async fn host_call_library_wait_any_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; if let Err(envelope) = require_capability( caller.data(), requirement_for_import("host_call_library_wait_any").unwrap(), ) { return write_guest_response(caller, &envelope).await; } let handles: Vec<u32> = match serde_json::from_slice(&bytes) { Ok(handles) => handles, Err(e) => { return write_guest_response( caller, &error_envelope("BAD_INPUT", format!("expected an array of handles: {e}")), ) .await; } }; let mut waiting = match caller.data_mut().pending_calls.take(&handles) { Ok(waiting) => waiting, Err(message) => { return write_guest_response(caller, &error_envelope("BAD_INPUT", message)).await; } }; let (handle, outcome) = waiting.first().await; caller.data_mut().pending_calls.restore(waiting); let settled = happyview_plugin_sdk::wire::LibraryCallSettled { handle, outcome }; write_guest_response(caller, &serde_json::to_vec(&settled).unwrap_or_default()).await}
async fn host_get_api_surface_impl( caller: &mut wasmtime::Caller<'_, PluginState>, lib_ptr: i32, lib_len: i32,) -> i64 { let Some(lib) = read_guest_string(caller, lib_ptr, lib_len) else { return 0; }; let executor = match library_access(caller.data(), "host_get_api_surface") { Ok(e) => e, Err(envelope) => return write_guest_response(caller, &envelope).await, }; let response = match executor.api_surface(&lib).await { Ok(surface) => { serde_json::to_vec(&serde_json::json!({"ok": &*surface})).unwrap_or_default() } Err(e) => error_envelope("LIBRARY_ERROR", e), }; write_guest_response(caller, &response).await}
/// Host function: run raw SQL. `execute` selects `host_db_execute` (needs/// `database:write`) vs `host_db_query` (needs either `database:read` or/// `database:write`, but read-only capability restricts it to read-only SQL)./// SQL is passed through untranslated with backend-native placeholders/// (`?` on SQLite, `$1`… on Postgres).async fn host_db_impl( caller: &mut wasmtime::Caller<'_, PluginState>, execute: bool, sql_ptr: i32, sql_len: i32, params_ptr: i32, params_len: i32,) -> i64 { let import = if execute { "host_db_execute" } else { "host_db_query" }; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(import).unwrap()) { return write_guest_response(caller, &envelope).await; } let (Some(sql), Some(params_bytes)) = ( read_guest_string(caller, sql_ptr, sql_len), read_guest_bytes(caller, params_ptr, params_len), ) else { return 0; }; let params: Vec<serde_json::Value> = if params_bytes.is_empty() { Vec::new() } else { match serde_json::from_slice(¶ms_bytes) { Ok(serde_json::Value::Array(a)) => a, _ => { return write_guest_response( caller, &error_envelope("BAD_INPUT", "params must be a JSON array"), ) .await; } } }; // database:read alone may only run queries; database:write may run anything through either import. if !caller .data() .capabilities .contains(&PluginCapability::DatabaseWrite) { match crate::raw_sql_guard::is_read_only(&sql) { Ok(true) => {} Ok(false) => { return write_guest_response( caller, &error_envelope( "FORBIDDEN", "database:read permits only read-only statements; declare database:write to modify data", ), ) .await; } Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, } } let external = caller .data() .app_state .as_ref() .and_then(|app| app.records.external().cloned()); if let Some(external) = &external && let Err(e) = crate::raw_sql_guard::check_claimed_record_writes(&sql, ¶ms, &|c| { external.claims(c) }) { return write_guest_response(caller, &error_envelope("FORBIDDEN", e)).await; } // SQL that reads the record views runs on the external storage's raw read // route, where those views show its records. It is classified before the // read-only gate: a read the gate cannot vouch for (a locking clause, SQL // the parser rejects) must not fall through to the local views, which hold // none of the claimed records. A write runs locally as it always has, the // claimed-write guard above covering claimed collections; one that also // reads the views sees only the local rows. if !execute && let Some(ext) = &external && let Some(route) = ext.raw_route() && !crate::raw_sql_guard::is_write(&sql) { match crate::raw_sql_guard::record_view_use(&sql) { Ok(crate::raw_sql_guard::RecordViews::None) => {} Err(e) => return write_guest_response(caller, &error_envelope("FORBIDDEN", e)).await, Ok(_) if crate::raw_sql_guard::is_read_only(&sql) != Ok(true) => { return write_guest_response( caller, &error_envelope( "FORBIDDEN", "raw SQL that reads claimed records must be a plain read-only query", ), ) .await; } Ok(crate::raw_sql_guard::RecordViews::Mixed(_)) => { return write_guest_response( caller, &error_envelope("FORBIDDEN", crate::raw_sql_guard::RAW_MIXED_REFUSED), ) .await; } Ok(crate::raw_sql_guard::RecordViews::Only) => { let response = match super::run_read_only_query_on( &route.pool, route.backend, &sql, ¶ms, ) .await { Ok(rows) => { serde_json::to_vec(&serde_json::json!({"ok": rows})).unwrap_or_default() } Err(super::QueryError::Database(e)) if ext.unavailable(&e) => { error_envelope(crate::error::STORAGE_UNAVAILABLE, e) } Err(e) => { let backend = match route.backend { crate::db::DatabaseBackend::Postgres => "postgres", crate::db::DatabaseBackend::Sqlite => "sqlite", }; error_envelope( "DB_ERROR", format!( "{e} (record views on this instance are served by a {backend} database)" ), ) } }; return write_guest_response(caller, &response).await; } } } let Some(db) = caller.data().db.clone() else { return write_guest_response(caller, &error_envelope("HOST_ERROR", "no database")).await; }; let response = if execute { match super::run_execute(&db, &sql, ¶ms).await { Ok(n) => serde_json::to_vec(&serde_json::json!({"ok": {"rows_affected": n}})) .unwrap_or_default(), Err(e) => error_envelope("DB_ERROR", e), } } else { match super::run_query(&db, &sql, ¶ms).await { Ok(rows) => serde_json::to_vec(&serde_json::json!({"ok": rows})).unwrap_or_default(), Err(e) => error_envelope("DB_ERROR", e), } }; write_guest_response(caller, &response).await}
/// `AtprotoError` → envelope code. Distinct codes so a plugin can tell "no/// signer configured" from "signature could not be checked" from an ordinary/// database or network failure, rather than seeing one undifferentiated/// `HOST_ERROR`.fn atproto_error_envelope(e: super::AtprotoError) -> Vec<u8> { use super::AtprotoError; match e { AtprotoError::Resolve(msg) => error_envelope("RESOLVE_ERROR", msg), AtprotoError::Blob { status, body } => { error_envelope("BLOB_ERROR", format!("PDS returned {status}: {body}")) } AtprotoError::NoSigner => error_envelope( "NO_SIGNER", "no attestation signer is configured on this instance", ), AtprotoError::Unverifiable(msg) => error_envelope("UNVERIFIABLE", msg), AtprotoError::Database(inner) => error_envelope("HOST_ERROR", inner), AtprotoError::Other(msg) => error_envelope("HOST_ERROR", msg), }}
/// Host function: resolve the AT Protocol service a DID's document/// advertises. Needs no session — it reads the public network, not the/// caller's own repo — so it runs off `app_state` alone.async fn host_atproto_resolve_service_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_atproto_resolve_service"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::AtprotoResolveService = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(app_state) = caller.data().app_state.clone() else { return write_guest_response( caller, &error_envelope( "HOST_ERROR", "this instance has no app state to resolve against", ), ) .await; }; let response = match super::resolve_service(&app_state.http, &app_state.config.plc_url, &spec.did).await { Ok(endpoint) => { serde_json::to_vec(&serde_json::json!({"ok": endpoint})).unwrap_or_default() } Err(e) => atproto_error_envelope(e), }; write_guest_response(caller, &response).await}
/// Host function: resolve a handle or a DID to both.////// `handle` comes back only when it was confirmed in both directions, which/// is what makes it safe to display; the DID is whatever the identifier/// resolved to.async fn host_atproto_resolve_identity_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_atproto_resolve_identity"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::AtprotoResolveIdentity = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(app_state) = caller.data().app_state.clone() else { return write_guest_response( caller, &error_envelope( "HOST_ERROR", "this instance has no app state to resolve against", ), ) .await; }; let response = match crate::identity::resolve_verified( &app_state.http, &app_state.config.plc_url, &spec.identifier, ) .await { Ok(identity) => serde_json::to_vec(&serde_json::json!({ "ok": happyview_plugin_sdk::wire::ResolvedIdentity { did: identity.did, handle: identity.handle, } })) .unwrap_or_default(), Err(e) => error_envelope("HOST_ERROR", e), }; write_guest_response(caller, &response).await}
/// Host function: store bytes under the CID of their content.////// The caller does not choose the key, so a plugin cannot file bytes under a/// CID they do not hash to, and storing the same content twice is the row/// already there.async fn host_blob_put_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_blob_put"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let req: happyview_plugin_sdk::wire::BlobPut = match serde_json::from_slice(&bytes) { Ok(r) => r, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(ctx) = build_host_context(caller.data()) else { return 0; };
let response = match crate::blobs::put(&ctx.db, ctx.db_backend, &req.bytes, &req.mime_type).await { Ok(cid) => serde_json::to_vec(&serde_json::json!({ "ok": { "cid": cid } })), Err(e) => serde_json::to_vec(&serde_json::json!({ "error": { "code": "BLOB_ERROR", "message": e.to_string(), "retryable": false } })), } .unwrap_or_default(); write_guest_response(caller, &response).await}
/// Host function: read a stored blob's bytes.async fn host_blob_get_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_blob_get"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let req: happyview_plugin_sdk::wire::BlobLookup = match serde_json::from_slice(&bytes) { Ok(r) => r, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(ctx) = build_host_context(caller.data()) else { return 0; };
let response = match crate::blobs::get(&ctx.db, ctx.db_backend, &req.cid).await { // Absent is `0`, the same "nothing here" a `kv` miss answers, rather // than an error: asking for a blob that is not held is an ordinary // outcome. Ok(None) => return 0, Ok(Some(blob)) => { let size = blob.bytes.len() as u64; serde_json::to_vec( &serde_json::json!({ "ok": happyview_plugin_sdk::wire::BlobData { bytes: blob.bytes, mime_type: blob.mime_type, size, } }), ) } Err(e) => serde_json::to_vec(&serde_json::json!({ "error": { "code": "BLOB_ERROR", "message": e.to_string(), "retryable": false } })), } .unwrap_or_default(); write_guest_response(caller, &response).await}
/// Host function: a stored blob's media type and size, without its bytes.////// Answering `0` for an absent blob is what lets a caller ask whether one is/// held without transferring it, so there is no separate existence import to/// drift from this one.async fn host_blob_stat_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_blob_stat"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let req: happyview_plugin_sdk::wire::BlobLookup = match serde_json::from_slice(&bytes) { Ok(r) => r, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(ctx) = build_host_context(caller.data()) else { return 0; };
let response = match crate::blobs::stat(&ctx.db, ctx.db_backend, &req.cid).await { Ok(None) => return 0, Ok(Some(stat)) => serde_json::to_vec( &serde_json::json!({ "ok": happyview_plugin_sdk::wire::BlobInfo { cid: req.cid, mime_type: stat.mime_type, size: stat.size as u64, } }), ), Err(e) => serde_json::to_vec(&serde_json::json!({ "error": { "code": "BLOB_ERROR", "message": e.to_string(), "retryable": false } })), } .unwrap_or_default(); write_guest_response(caller, &response).await}
/// Host function: download a blob from a repo. Same "no session needed" shape/// as service resolution — the DID is a parameter of the request, not the/// caller's own identity.async fn host_atproto_blob_download_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_atproto_blob_download"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::AtprotoBlobDownload = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(app_state) = caller.data().app_state.clone() else { return write_guest_response( caller, &error_envelope( "HOST_ERROR", "this instance has no app state to resolve against", ), ) .await; };
// A blob download is an HTTP fetch like any other and counts against the // same shared budget `host_http_request` does — it just goes through PDS // resolution instead of a plugin-supplied URL. There is no // `allowed_hosts` check to make here: the host, not the plugin, chose // the PDS host, via DID resolution inside `super::blob_download`. let requests_over_budget = { let usage = &mut caller.data_mut().usage; usage.http_requests += 1; (usage.http_requests > super::MAX_HTTP_REQUESTS).then_some(usage.http_requests) }; if let Some(requests) = requests_over_budget { return write_guest_response( caller, &error_envelope( "BLOB_ERROR", format!( "too many requests: {requests} > {}", super::MAX_HTTP_REQUESTS ), ), ) .await; }
// `host_http_request` refuses once the transfer budget is already spent, // before sending — checked here too, not just after the fetch below, so // an exhausted budget can't still push through one more multi-hundred-MB // blob before the next call finally sees it. let already_over_budget = caller.data().usage.http_bytes_transferred > super::MAX_HTTP_TOTAL_TRANSFER; if already_over_budget { return write_guest_response( caller, &error_envelope( "BLOB_ERROR", format!( "transfer limit exceeded: {} > {}", caller.data().usage.http_bytes_transferred, super::MAX_HTTP_TOTAL_TRANSFER ), ), ) .await; }
let blob = match super::blob_download(&app_state.http, &app_state.config.plc_url, spec).await { Ok(blob) => blob, Err(e) => return write_guest_response(caller, &atproto_error_envelope(e)).await, };
let transfer_over_budget = { let usage = &mut caller.data_mut().usage; usage.http_bytes_transferred += blob.size; (usage.http_bytes_transferred > super::MAX_HTTP_TOTAL_TRANSFER) .then_some(usage.http_bytes_transferred) }; if let Some(transferred) = transfer_over_budget { return write_guest_response( caller, &error_envelope( "BLOB_ERROR", format!( "transfer limit exceeded: {transferred} > {}", super::MAX_HTTP_TOTAL_TRANSFER ), ), ) .await; }
let response = serde_json::to_vec(&serde_json::json!({"ok": blob})).unwrap_or_default(); write_guest_response(caller, &response).await}
/// Host function: look up labels for a set of URIs, keyed by URI.async fn host_labels_get_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_labels_get"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::LabelsGet = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let Some(db) = caller.data().db.clone() else { return write_guest_response(caller, &error_envelope("HOST_ERROR", "no database")).await; }; let records = records_or_local(caller.data(), &db); let response = match super::labels_get(&records, &spec.uris).await { Ok(map) => serde_json::to_vec(&serde_json::json!({"ok": map})).unwrap_or_default(), // `labels_get`'s only source of `Other` is the URI-count cap — a // caller-fixable mistake, not a host failure, so it gets `BAD_INPUT` // here rather than the `HOST_ERROR` the shared envelope maps `Other` // to for the other imports. Err(super::AtprotoError::Other(msg)) => error_envelope("BAD_INPUT", msg), Err(e) => atproto_error_envelope(e), }; write_guest_response(caller, &response).await}
/// Host function: sign a record with this instance's attestation key. Reads/// `attestation_signer` off `app_state` — this import needs no session, so it/// cannot get the signer any other way.async fn host_attest_sign_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_attest_sign"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::AttestSign = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let signer = caller .data() .app_state .as_ref() .and_then(|s| s.attestation_signer.clone()); let caller_did = caller.data().call_ctx.caller_did.clone();
// The DID is part of the signed content, so an absent caller can't fall // back to signing as `""` — that produces a signature that can never // verify against a real repo. A missing signer is checked first (and // reported as `NO_SIGNER` below) so a plugin with neither configured // still gets the more specific reason for the failure it actually hit. if signer.is_some() && caller_did.is_none() { return write_guest_response( caller, &error_envelope("BAD_INPUT", "signing needs a caller"), ) .await; } let did = caller_did.unwrap_or_default(); let response = match super::attest_sign(signer.as_deref(), &did, spec.record) { Ok(sig) => serde_json::to_vec(&serde_json::json!({"ok": sig})).unwrap_or_default(), Err(e) => atproto_error_envelope(e), }; write_guest_response(caller, &response).await}
/// Host function: verify a record's attestation signature.async fn host_attest_verify_impl( caller: &mut wasmtime::Caller<'_, PluginState>, req_ptr: i32, req_len: i32,) -> i64 { const IMPORT: &str = "host_attest_verify"; if let Err(envelope) = require_capability(caller.data(), requirement_for_import(IMPORT).unwrap()) { return write_guest_response(caller, &envelope).await; } let Some(bytes) = read_guest_bytes(caller, req_ptr, req_len) else { return 0; }; let spec: happyview_plugin_sdk::wire::AttestVerify = match serde_json::from_slice(&bytes) { Ok(s) => s, Err(e) => return write_guest_response(caller, &error_envelope("BAD_INPUT", e)).await, }; let signer = caller .data() .app_state .as_ref() .and_then(|s| s.attestation_signer.clone()); let response = match super::attest_verify(signer.as_deref(), spec) { Ok(valid) => serde_json::to_vec(&serde_json::json!({"ok": valid})).unwrap_or_default(), Err(e) => atproto_error_envelope(e), }; write_guest_response(caller, &response).await}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_plugin_state_fields_exist() { fn _check_fields(state: &PluginState) { let _ = &state.plugin_id; let _ = &state.scope; let _ = &state.secrets; let _ = &state.config; let _ = &state.usage; let _ = &state.memory; let _ = &state.alloc; let _ = &state.dealloc; } }
/// Every field filled by hand, the way `PluginExecutor::instantiate` /// does — no `Default` impl exists for `PluginState`, since a real /// instance is never built any other way. Tests that only care about one /// or two fields (here, `allowed_hosts`) use this rather than each /// re-deriving the rest. fn test_plugin_state(allowed_hosts: Vec<String>) -> PluginState { crate::db::install_drivers(); PluginState { plugin_id: "test-plugin".into(), scope: "did:example:test".into(), secrets: HashMap::new(), config: serde_json::json!({}), db: None, db_backend: crate::db::DatabaseBackend::Sqlite, http_client: reqwest::Client::new(), lexicons: Arc::new(crate::catalog::LexiconCatalog::detached()), usage: Default::default(), memory: None, alloc: None, dealloc: None, capabilities: std::collections::HashSet::new(), allowed_hosts, plugin_type: crate::plugin::PluginType::Library, executor: None, call_ctx: crate::plugin::library::LibraryCallContext::default(), depth: 0, caller: None, script_run: None, app_state: None, wasi: super::super::build_wasi_context( "test-plugin", None, crate::db::DatabaseBackend::Sqlite, ), deadline: crate::plugin::runtime::GuestDeadline::unbounded(Arc::new( std::sync::atomic::AtomicU64::new(0), )), limiter: crate::plugin::runtime::MemoryLimiter::new( crate::plugin::runtime::LIBRARY_MEMORY_CEILING, ), pending_calls: Default::default(), panic: None, call_fuel: u64::MAX, } }
/// Direct construction with no hosts set — the default a plugin sees /// before an operator has configured `network:request:defined`, or under /// any capability that grants none. #[test] fn resolved_allowed_hosts_defaults_to_empty() { let state = test_plugin_state(Vec::new()); assert!(resolved_allowed_hosts(&state).is_empty()); }
#[test] fn resolved_allowed_hosts_returns_the_resolved_list() { let hosts = vec![ "api.example.com".to_string(), "*.cdn.example.com".to_string(), ]; let state = test_plugin_state(hosts.clone()); assert_eq!(resolved_allowed_hosts(&state), hosts); }
/// The executor recovers a 401 by finding the prefix anywhere in the /// error string, so the envelope has to carry it out of the guest. #[test] fn an_auth_failure_leaves_with_the_prefix_the_executor_looks_for() { let envelope = error_envelope( super::super::CallerError::Auth("expired".into()).code(), caller_error_message(&super::super::CallerError::Auth("expired".into())), ); let parsed: serde_json::Value = serde_json::from_slice(&envelope).unwrap(); assert_eq!(parsed["error"]["code"], "AUTH_REQUIRED"); let message = parsed["error"]["message"].as_str().unwrap(); assert!( message.contains(crate::error::LUA_AUTH_ERROR_PREFIX), "{message}" ); assert!(message.contains("expired"), "{message}"); }
/// Only an auth failure gets the prefix; anything else wearing it would /// turn an unrelated error into a spurious logout. #[test] fn other_failures_are_not_dressed_as_auth_failures() { let message = caller_error_message(&super::super::CallerError::Pds { status: 400, body: "InvalidSwap".into(), }); assert!( !message.contains(crate::error::LUA_AUTH_ERROR_PREFIX), "{message}" ); }
#[test] fn test_pack_ptr_len() { let ptr: u32 = 0x1000; let len: u32 = 0x0100; let packed: i64 = ((ptr as i64) << 32) | (len as i64); let unpacked_ptr = (packed >> 32) as u32; let unpacked_len = (packed & 0xFFFFFFFF) as u32; assert_eq!(unpacked_ptr, ptr); assert_eq!(unpacked_len, len); }
#[test] fn test_bounds_check_helper() { assert!(check_bounds(0, 10, 100).is_ok()); assert!(check_bounds(90, 10, 100).is_ok()); assert!(check_bounds(91, 10, 100).is_err()); assert!(check_bounds(0, 0, 100).is_ok()); }}