35 KiB
Native modules
@ps5-react/core exposes platform services as React Native-style modules.
Apps import them like any other export; they never touch the SDK, libkernel,
or SDL directly.
import {Platform, DeviceInfo, FileSystem, Controller, useGamepad} from '@ps5-react/core';
const {model, firmware} = DeviceInfo.get();
FileSystem.writeFile(`${FileSystem.dataDir}/save.json`, JSON.stringify(state));
Controller.setLightBar('#38bdf8');
apps/system-explorer exercises every module.
Contract
The modules in runtime/js/native.js wrap globalThis.__ps5ReactNative, which
each host installs before the bundle runs. Its exact shape (ABI v15) is
documented in native/shared/host_api.hpp.
native/shared/host_api.cpp implements the filesystem with POSIX and the object
itself; native/ps5/ and native/desktop/ implement the platform functions.
- Synchronous bridge. Each native call returns directly. Networking calls
only submit/cancel/poll bounded tasks;
Http.requestandtask.doneexpose promises in the JavaScript wrapper while native workers perform I/O. - Render thread. Calls run on the same thread as React and the rasterizer. Keep them cheap: avoid listing large directories or reading files every frame. Call them from event handlers, effects, or state initializers rather than repeatedly during render.
- Errors. A failed call throws an
Errorwhose message names the call, the path, and the system error, for examplefs.readDir /download0/missing: No such file or directory. Wrap calls intry/catchand show the message. - Missing host. If
__ps5ReactNativedoes not exist (for example when a module is evaluated outside a PS5 React host), every call throws an error explaining that a PS5 React host is required. - QuickJS runtime. The runtime has no
Dateobjects (Date.now()works), typed arrays, orProxy; values cross the boundary as strings, numbers, booleans, plain objects, arrays, andnull.
Platform
| Member | Returns | Notes |
|---|---|---|
Platform.OS |
'ps5' | 'desktop' |
The running host |
Platform.select(spec) |
spec[OS], else spec.default |
Keys: ps5, desktop, default |
const label = Platform.select({ps5: 'Console', desktop: 'Preview', default: 'Unknown'});
DeviceInfo
DeviceInfo.get() returns:
| Field | Unit | Notes |
|---|---|---|
model |
string | Console model, or Desktop preview (<Mac model>) |
firmware |
string | System software version, or macOS <version> |
cpuTemperature |
°C | null on desktop |
socTemperature |
°C | null on desktop |
cpuFrequency |
Hz | null when unknown (always on Apple silicon) |
freeMemory |
bytes | Memory available to the app |
processTime |
µs | CPU time used by this process |
language |
string | System language as a BCP 47 tag (en-US, pt-BR, zh-Hans) |
Any value the host cannot read is null.
DeviceInfo.utcOffset(time = Date.now()) (ABI v14) returns the system time
zone's offset from UTC at that instant in minutes, summer time included: 120
for Central European Summer Time, -300 for US Eastern Standard Time. With no
Date objects in the runtime, local time is arithmetic on
time + utcOffset(time) * 60000. Pass the instant you are converting, not the
current time, when it may fall on the other side of a summer-time change. The
PS5 reads the zone set in the console's settings; the desktop preview the Mac's.
FileSystem
By default, the title runs sandboxed. App paths are logical roots; explicit console paths become accessible only after opt-in filesystem access succeeds.
| Constant | Path | Access |
|---|---|---|
FileSystem.appDir |
/app0 |
The installed application folder; read-only |
FileSystem.dataDir |
/download0 |
Writable, persists between launches |
FileSystem.tempDir |
/temp0 |
Writable scratch space |
On the console, listing /app0 may return an empty array even though files
inside it can be read by path. Paths containing .. components are rejected.
| Function | Returns | Notes |
|---|---|---|
readDir(path) |
FileStat[] |
Entries of a directory, without . and .. |
stat(path) |
FileStat | null |
null when the path does not exist |
exists(path) |
boolean |
stat(path) !== null |
readFile(path) |
string |
UTF-8; throws above 8 MiB |
writeFile(path, text, {sync}) |
— | Creates or truncates; sync: true flushes it to the disk before returning, for a file saved then renamed into place |
appendFile(path, text) |
— | Creates or appends |
mkdir(path, {recursive}) |
— | recursive defaults to false |
remove(path) |
— | A file or an empty directory |
removeTree(path) |
— | A file, or a directory and everything in it; symbolic links are unlinked, never followed. Synchronous: for small app-owned trees |
removeAsync(path) |
Promise<void> |
What removeTree removes, on a worker thread, one path at a time in the order asked. Resolves once the path is gone, or if it never existed; rejects with an Error naming the path and the system error. For large files and trees, whose removal holds the screen for seconds |
rename(from, to) |
— | Both are app paths |
chmod(path, mode) |
— | Permission bits, such as 0o777; a file or directory the console's own services must read (a title's files) needs them open to every user |
mounts() |
{device, path, type}[] |
Up to 64 mounted filesystems visible to this process, as host paths; see Hardware status |
diskUsage(path) |
{total, free} |
Bytes for the filesystem holding path; free is available to the app, clamped to 0..total |
FileStat is {name, isDirectory, isFile, size, modified, device, inode}, where
size is in bytes and modified is milliseconds since the Unix epoch.
device and inode are decimal strings that preserve native 64-bit identities.
Use both to detect replacement of a directory or file, including another drive
mounted at the same path; these identities are filesystem-local, not universal
drive serial numbers.
Files are text only; there is no binary read or write.
For console-wide filesystem access, add this to the app manifest:
{ "filesystemAccess": "console" }
Then query storage from an event handler or effect:
const entries = FileSystem.readDir('/data');
const mounts = FileSystem.mounts();
const {total, free} = FileSystem.diskUsage('/data');
Keep filesystemAccess set to "sandbox" (the default) for apps that only
need their own files. The opt-in helper requires resident Lapy or a local ELF
loader; see access requirements. On failure, filesystem
calls retain their normal permission errors rather than returning fake values.
Http and Downloads
Http.request(url, options) provides bounded text/JSON HTTP responses, effective
URLs and allowlisted lowercase response headers. Set followRedirects: false
to inspect redirect metadata; cookies and authentication headers stay private.
HTTP requests accept a custom Range header for bounded probes; downloads own
their ranges and reject it. Custom headers disable automatic redirects. Response
headers include etag and last-modified for stable file identity checks.
Downloads.enqueue({url, destination, ...options}) returns a cancellable task
with progress subscriptions and a completion promise. Binary downloads stay
native, use parallel validated ranges when eligible, and resume durable ranges.
Downloads.enqueueManifest({manifest, destination, ...options}) supports split
byte manifests with per-piece SHA-1 and whole-file SHA-256 verification.
DownloadFormats validates manifests and routes image, package, archive and
binary filenames to their appropriate destinations. Native download options
include opt-in rejectHtml, exact expectedBytes, storageRoot identity checks, recoverCompleted
for receipt-backed SHA-256 verification of published files, and normalized
pieces: [{url, offset, size, sha1}]. These options share ABI v2 on both hosts;
there are no additional global native functions.
Downloads.setRateLimit(bytesPerSecond) (ABI v17) caps what all file downloads
receive together, 0 for no limit. It applies at once, running downloads included;
a capped download opens about one connection per MB/s. A download enqueued with
unlimited: true, such as an app's own update, is exempt.
PS5 builds require networking: true and filesystemAccess: "console"; desktop
networking is available by default. See NETWORKING.md for
options, resource limits, errors, lifecycle and the hardware-validation gap.
LocalNetwork (ABI v9)
LocalNetwork.broadcast({port, message, timeoutMs, maxReplies, signal}) sends one UDP
datagram to 255.255.255.255:port and resolves with the replies gathered for
timeoutMs (100–10000, default 1500) or until maxReplies (1–64, default 16) arrive:
[{address, port, text}], where address is the sender's IPv4 address. It is for
service discovery, such as Jellyfin's:
import {LocalNetwork} from '@ps5-react/core';
const replies = await LocalNetwork.broadcast({port: 7359, message: 'who is JellyfinServer?'});
The query runs on a native worker of its own; the bridge (lan.broadcast, lan.cancel,
lan.poll) only submits, cancels and polls. Messages are at most 1 KiB and each reply
is cut at 4 KiB; four queries run at a time, and a fifth throws. A signal cancels the
query and rejects with an AbortError. Anyone on the network can reply with anything:
check a reply against its sender before using an address it names. It needs
networking: true (and on PS5 filesystemAccess: "console"), like HTTP. Only IPv4
limited broadcast is sent; multicast (SSDP, mDNS) and unicast UDP are not available.
Images
<Image source={{uri}}> loads http:// and https:// URLs natively: fetched
and decoded by two workers off the render thread, at the size the element is
drawn, into a bounded LRU cache. Image.prefetch(uri, {width, height, resizeMode}) warms that cache. Image.getColor(uri) resolves to the art's most
prominent vivid colour as '#rrggbb', or null for grey or dark art, computed
while decoding a small copy. The bridge (image.load, image.release,
image.poll, ABI v3; results carry that color; image.warm and image.defer(ms), which
ScrollViews call while they move to hold network requests back) is internal to the Image component. See
IMAGES.md for sizing, formats, memory budgets and errors.
Notifications
Notifications.show(message, subMessage?) shows a system notification and
returns true when the host accepted it.
On PS5 it uses libkernel's sceKernelSendNotificationRequest: a plain toast whose
subMessage follows the message on a second line.
Users
| Function | Returns |
|---|---|
Users.getForeground() |
{id, name} | null — the user who launched the app |
Users.getLoggedIn() |
{id, name}[] |
Controller
| Function | Notes |
|---|---|
setLightBar(color) |
'#rrggbb' or {r, g, b} with 0–255 channels |
resetLightBar() |
Restores the system color |
vibrate(strength = 1, ms = 200) |
strength 0–1 |
setTriggerEffect(trigger, effect) |
ABI v12. A DualSense adaptive trigger effect on 'l2', 'r2' or 'both', replacing the last one; effects below. The PS5 turns the triggers off when the app quits; the desktop preview logs the request without sending it |
setTriggerResistance(trigger, strength, from = 0) |
setTriggerEffect(trigger, {type: 'resistance', from, strength}) |
getState() |
GamepadState, the latest polled state |
subscribe(listener, {frames = false}) |
Calls listener(event) once per frame with the controller's events, below; returns the function that stops it |
Trigger effects take positions as fractions of the travel (0 top, 1 bottom) and
strengths 0–1 in the controller's eight steps, where 0 turns the effect off. An
invalid effect throws a RangeError naming the field:
| Effect | Feel |
|---|---|
{type: 'off'} |
A free trigger |
{type: 'resistance', from = 0, strength} |
Pushes back from from to the bottom (libScePad mode 1, feedback) |
{type: 'weapon', start, end, strength} |
Resists between start and end, then gives way with a click, like a gun trigger (mode 2). The controller places them in tenths of the travel: start lands in 0.2–0.8 and end at least a tenth past it, at most 0.9 |
{type: 'vibration', from = 0, amplitude, frequency} |
Buzzes from from on, at frequency Hz, 1–255 (mode 3) |
GamepadState is:
{
connected: boolean;
leftX: number; leftY: number; // -1..1, deadzone applied
rightX: number; rightY: number; // -1..1, deadzone applied
l2: number; r2: number; // 0..1
buttons: string[]; // held: up down left right cross circle triangle
// square l1 r1 l2 r2 l3 r3 options touchpad
}
The host updates the state once per frame, before JavaScript timers and controller actions run.
Controller events
useControllerEvents(handler, {frames = false}) (or Controller.subscribe) diffs
the state once per frame, in the frame callback that steps motion, and calls the
handler for each change: what an action cannot tell, such as a release, how far a
trigger is in, or the touchpad and L3/R3, which send no action. All listeners
share one read of the state, made only while any listens.
| Event | When |
|---|---|
{type: 'press', button, state} |
A GamepadState button went down; L2 and R2 at the host's press threshold (half travel) |
{type: 'release', button, heldMs, state} |
It came up, after heldMs of frame time |
{type: 'trigger', trigger, value, previous, state} |
'l2' or 'r2' travel changed, 0–1 |
{type: 'frame', elapsedMs, state} |
With frames, after the others every frame, for work that steps while something is held |
A frame delivers releases, then presses, then trigger changes, so a release ends
before the next press starts. A button already held when a listener starts is not
a press for it (it may be the press that mounted the listener); its release is.
Pass null as the handler to stop listening. The events are a diff of per-frame
states: a press and release within one frame are not seen, while the host's
actions (useController) still arrive.
function SkipIntro({onSkip}) {
useControllerEvents(event => {
if (event.type === 'press' && event.button === 'touchpad') onSkip();
});
return <Text>Press the touchpad to skip</Text>;
}
useGamepad(intervalMs = 0) reads the state on the same per-frame reader while
the component is mounted (at most once per intervalMs when given) and
re-renders only when it changes:
function Sticks() {
const pad = useGamepad();
return <Text>{pad.leftX} {pad.leftY} {pad.buttons.join(' ')}</Text>;
}
Menus use focus navigation (NAVIGATION.md). useController
receives the raw up, down, left, right, confirm, back, l1, r1,
l2, r2, triangle, and square actions (plus previous/next for
compatibility); useControllerEvents is for releases, holds and analog travel,
and useGamepad for drawing raw state. Options arrives as the options action, and the PS button
leaves the app (NAVIGATION.md).
Sound
Short interface sounds: focus ticks, confirm, back, errors, notifications. Import a WAV like an image; the build bakes it into the app and the import is the sound's name:
import {Sound, useNavigationEvents} from '@ps5-react/core';
import tick from './assets/sounds/focus.wav';
useNavigationEvents(event => { if (event.type === 'move') Sound.play(tick); });
| Function | Notes |
|---|---|
Sound.play(name, {volume = 1, pan = 0}) |
Returns true when the sound started; volume 0–1, pan -1 (left) to 1 (right) |
Sound.setVolume(volume) |
Master volume 0–1 for every sound (0 mutes) |
- Format. 16-bit PCM WAV, mono or stereo, 8–96 kHz, at most 10 seconds; other files fail the build with the file name. Keep sounds short: they stay decoded in memory (8 bytes per frame). An app that imports no WAV opens no audio output.
- One voice per sound. While a sound still plays, playing it again is
skipped and returns
false, so a held D-pad keeps ticking without stacking. Different sounds overlap. - Cheap and non-blocking.
playposts a command to a lock-free queue and returns; mixing runs on an audio thread (the ps5-homebrew-ui mixer: 32 voices, 48 kHz stereo, a soft limiter). It returnsfalsewhen there is no audio output. An unknown name throwssound.play <name>: no imported .wav has this name. - Feedback for navigation.
useNavigationEvents(NAVIGATION.md) reports each move, blocked direction, press, back and button action, so one hook at the root gives every screen its sounds.
apps/starter/sounds.js wires the framework's sound set (focus, confirm,
back, error, page, notify) this way, and npm run create copies it. The
set is derived from Google's Material Design sound resources (CC BY 4.0): keep
licenses/material-sounds-NOTICE.txt with an app that ships it. node tools/ui_sounds.mjs <material wav directory> <output directory> rebuilds it from
the pack's WAV files.
Linking
Linking.openURL(url) opens an http:// or https:// URL in the system
browser and returns true when the host launched it. Other schemes throw.
Power
Power.keepAwake(enabled) keeps the console out of rest mode for inactivity
while true, for work that runs with nobody touching the controller, such as a
download of several hours. Turn it on when the work starts and off when it
ends; the last call wins, so an app with several jobs keeps one count of its
own.
The PS5 host calls sceSystemServicePowerTick every 30 seconds while it is on,
which restarts the inactivity timer as input does (the method FTP and download
homebrew use). The desktop preview disables or enables the SDL screen saver and
prints [power] keep awake on|off. Not yet validated on hardware: no test
has shown a console staying awake through its rest-mode timeout.
Video (ABI v10)
<Video> (VIDEO.md) is built on video.open(source, options), which returns the
player's id and the name of the placeholder image its <Image layer> draws, video.close(id),
setters for pause, seek, volume, mute, loop, resize mode, audio delay, audio output (ABI v13), audio and subtitle track,
video.addSubtitle(id, source, language, title) for a subtitle file, and video.poll(), every open
player's status, read each frame while one is open. Apps use the component.
AudioOutput (ABI v13)
AudioOutput.capabilities() returns what the TV or AV receiver the console outputs to takes, read
from its EDID at the call: {known, name, formats, passthrough, pcmChannels}. formats lists each
codec the EDID's audio descriptors name ({codec, channels, sampleRates, bitDepths?, maxBitrate?}),
passthrough the codecs a <Video audioOutput="auto"> sends it undecoded ('ac3', 'eac3',
'dts'), pcmChannels the most PCM channels it takes. VIDEO.md has the
fields and how passthrough plays. The PS5 host reads the EDID with sceVideoOutGetHdmiRawEdid_; the
desktop preview has none and returns known: false with empty lists.
Display
Display.refreshRate() returns the rate in Hz the display presents at.
Display.setRefreshRate(hz) (ABI v11) switches between 60 and 120 Hz while the
app runs, for example 120 Hz for film playback, where 24 fps divides evenly,
and 60 Hz for a heavy interface. The rate must be listed in app.json
"refreshRates"; any other throws.
The switch happens after the current frame. The PS5 host can change the rate
only while EGL is shut down, so it releases the presenter, closes and reopens
the display at the new rate, and builds the presenter again. JavaScript state,
React and the engine's framebuffer live in CPU memory and are kept; the first
frame afterwards uploads the whole framebuffer. The screen is black while the
TV resyncs, a second or two, and motion does not advance across that gap. A TV
without 120 Hz input stays at 60, and refreshRate() reports the accepted mode
once a frame has been presented at it. Asking for the current rate cancels a
switch that has not happened yet.
The desktop preview keeps the monitor's rate, rebuilds its presenter the same
way, reports the requested rate and prints
[PS5-REACT] refresh switch to <hz> Hz. Not yet validated on hardware: no
console has switched rates at runtime yet.
Fonts
Fonts.setLanguage(tag) sets the interface language (a BCP 47 tag such as
'ja', 'zh-Hans' or 'ar') for text drawn from the runtime fonts an app
packages with textFonts (International text). It chooses the
Japanese or the Chinese form of Han characters and the language text is shaped
for. It starts as DeviceInfo language; call it before text renders, since
text already laid out keeps its layout until it changes. Not yet validated on
hardware.
BackHandler
BackHandler.exitApp() asks the host to close the app after the current frame,
exactly like pressing Options in an app that leaves Options to the host (React Native's name for
the same call).
BackHandler.exitApp({relaunch: true}) also opens the app again once it has closed, and returns
whether that was arranged. An app cannot launch itself after it exits, so on PS5 it sends
relauncher.elf (native/ps5/payloads/relauncher.c, shipped in the app folder) to the payload
loader on 127.0.0.1:9021; the payload waits for the title to close, then launches it for the
foreground user. Without the loader it returns false, logs why, and the app only closes. The
desktop preview runs itself again.
Desktop preview behavior
npm run dev and npm run preview implement the same API on macOS:
- Files live in
.build/<app>/sandbox/:download0/andtemp0/are directories, andapp0is a link toapps/<app>/./lists the sandbox root. Unlike the console,/app0is writable on desktop; do not rely on it. mounts()lists the Mac's mounted filesystems.Notifications.showprints[notify] message: subMessageto the terminal.Usersreports the macOS user ($USER) with id1.- Temperatures are
null;cpuFrequencyisnullon Apple silicon. languagecomes fromPS5_REACT_LANGUAGE, elseLC_ALL,LC_MESSAGESorLANG(pt_BR.UTF-8readspt-BR);nullfor theC/POSIXlocale.openURLopens the URL in the default macOS browser.- With an SDL game controller connected, the light bar and rumble use it;
otherwise the calls print a
[pad]line. Trigger effects always print one. Without a controller,getState()reportsconnected: false, with the keys' buttons and travel (NAVIGATION.md): Z and C are L2 and R2 at full travel, or 0.4 with Shift, and P the touchpad. - Tools hold buttons and set trigger travel with the control channel's
hold,releaseandtrigger(tools/ps5_drive.py) or the same input-script steps. - Sounds play through SDL's default output.
SDL_AUDIODRIVER=dummykeeps the preview silent; the self-test sets it. AudioOutput.capabilities()reportsknown: false, and<Video>decodes every track: there is no HDMI sink to read or to pass a bitstream to.
Hardware status
The PS5 host implements this API against the public PS5 Payload SDK.
It has not yet been validated on hardware except where
HARDWARE.md records a test for a specific artifact, firmware,
and loader. Desktop behavior and successful PS5 builds do not establish console
behavior; individual calls may fail or return null on a given firmware.
Some PS5 calls load a system module at runtime. It is unverified whether a title can load these modules; when one cannot, only the dependent call degrades:
| Call | Module | Without it |
|---|---|---|
DeviceInfo.get().model |
libkernel_sys.sprx |
null |
Notifications.show() |
libSceNotification.sprx |
Returns false |
PS5 diskUsage() uses the native-title _fstatfs export on a descriptor opened
with sceKernelOpen. mounts() discovers kernel-provided records through
accessible descriptors, deduplicates them, and returns up to 64 records. Discovery
checks sandbox roots, common storage paths, and immediate children of / and
/mnt, with at most 96 metadata queries. It is not the privileged global mount
table returned by a payload's getmntinfo; inaccessible or deeper mounts can be
absent. No mount records or capacities are invented.
Hardware logs confirmed that our removed direct-syscall prototype terminated the
title with SYSTEM_ILLEGAL_FUNCTION_CALL. The replacement uses libkernel's
native wrapper, following a community title reference. The replacement
filesystem integration was confirmed by the user on firmware 13.60 with kstuff
and ShadowMount; exact application/helper hashes are recorded in
Hardware evidence. Directory records from sceKernelGetdents are bounds-checked before
names reach JavaScript. See Storage investigation.
Standalone titles can still receive EPERM even with kstuff running. For a
console filesystem explorer, set "filesystemAccess": "console" in app.json
(default: "sandbox"). The build packages a pinned upstream Lapy one-shot
helper restricted to that title. Startup requests access before creating the
React thread and accepts success only after a write/read/remove proof in /data.
The console must provide resident Lapy or an ELF loader on localhost port 9021.
Startup can take several seconds; no access requests run during React renders.
After success, /app0, /download0, and /temp0 remain logical app paths mapped
to their accessible sandbox mounts. Data and temporary roots must pass an
exclusive file creation, write, read and removal proof; opening a directory
for reading alone does not establish write access. If the original roots fail
that proof after the root change, data falls back to
/data/ps5-react/<TITLE_ID> and temporary files to its tmp directory. Directory
creation and fallback proofs are checked and failures log the path and errno.
That fallback persists until explicitly removed.
Failure leaves console path mapping disabled and logs the upstream status.
Optional native API modules are resolved before requesting the root change.
Desktop behavior is unchanged. The opt-in integration was user-confirmed on
firmware 13.60 with kstuff and ShadowMount. Other firmware/loader combinations
require their own validation.
diskUsage measures a filesystem, not recursive directory size or the console
Settings storage categories. On desktop it reports the real Mac filesystem
backing the per-app sandbox.
System Explorer caches these queries when its Files page opens. Navigate to a directory or select Refresh to query again; focus changes do not query storage.
Not yet available
- Global
fetch, raw sockets, multicast and WebSocket (UDP broadcast queries:LocalNetwork). - Music, streamed or long audio, and decoding compressed formats at run time.
- The system on-screen keyboard (IME) for text input.
- System save data; use
FileSystem.dataDirfor now.
Package installation
import {Packages} from '@ps5-react/core';
const task = Packages.install({path: '/data/downloads/packages/CUSA04286-game.pkg', name: 'BATTLESHIP'});
const unsubscribe = task.subscribe(({status, written, total}) => console.log(status, written, total));
try { await task.done; } finally { unsubscribe(); }
Installs a .pkg or .fpkg under /data, /mnt/usb* or /mnt/ext* through
the console's install service (sceAppInstUtilInstallByPackage), PS4 and PS5
packages alike. An app may not call that service, so each install sends
pkg-installer.elf, built from native/ps5/payloads/pkg_installer.c and
shipped in the app folder, to the payload loader on 127.0.0.1:9021; a jailbroken
console without that loader fails with an error naming the port. The snapshot
carries the console's install status (transferring, promoting,
playable), written/total bytes and the package's contentId; the task
completes once the title is playable. One install runs at a time. cancel()
stops reporting only: the console finishes an install it accepted. The preview
fails each request. Fake packages need a kernel that accepts them (kstuff); on
firmware above 11.60, PS5 fake game packages install but do not launch.
Archive extraction
import {Archives} from '@ps5-react/core';
const task = Archives.extract({
sources: ['/data/downloads/part-1.bin', '/data/downloads/part-2.bin'],
destination: '/data/downloads/extracted/my-release',
maxBytes: 100 * 1024 ** 3,
password: 'published-archive-password', // optional; case and brackets are significant
});
const unsubscribe = task.subscribe(({state, written}) => console.log(state, written));
try { const {artifacts} = await task.done; console.log(artifacts); }
finally { unsubscribe(); }
cancel() requests cancellation. ZIP, TAR, 7z and RAR/RAR5 use libarchive in a
worker thread. Supply ordered archive volumes; no full joined copy enters JS or
RAM. One extraction may run at a time. Source files are preserved. Output paths,
links, entry counts and byte budgets are validated. Extraction publishes a staged
directory only after successful reading and SHA-256 output receipts. A matching
published directory is reverified; matching interrupted staging is restarted.
Unowned destinations/staging are preserved. Image artifacts are flattened to the
output root for ShadowMount scan depth; duplicate filenames fail without overwrite.
password is optional, with at most 1024 UTF-8 bytes and no NUL. Wrong or missing
passwords fail without publishing partial output or removing source volumes.
ZIP decryption uses libarchive's passphrase support. RAR5 AES-256 headers/data are
decrypted in bounded buffers by the native reader before libarchive decompresses
them. Header CRCs, file checksums, decoder-memory limits and output-path validation
remain enforced. CBC padding is removed using compressed-block framing; no joined
or decrypted archive copy is written to disk. Ordered encrypted RAR5 volumes use
the same API. Optional quick-open (QO) service indexes are skipped; extraction
reads the original headers and verifies every file password and output CRC32.
An incomplete index payload or invalid enclosing header CRC still fails.
RAR5 files without CRC32, keyed CRC/BLAKE checksums, encrypted older RAR formats and
encrypted 7z are not supported. Password derivation is capped at 2^20 iterations
and runs on the extraction worker, never the render thread.
Unsupported encryption/compression methods and RAR5 dictionaries above libarchive's supported limit fail explicitly. Before extracting, the headers are read to refuse archives whose declared decoder windows (7z LZMA/LZMA2/PPMd/zstd and BCJ2 buffers, RAR5 windows, RAR PPMd, ZIP LZMA/XZ/zstd/PPMd, and an outer xz/zstd/lzip/lzma layer) exceed 64 MiB, since libarchive has no memory limit and the title's heap is fixed. That budget assumes nothing else large runs during extraction: don't extract while downloading. Self-extracting executables are refused. This API does not install PKGs or register games. Desktop extraction and successful PS5 linking are not console execution evidence.
Archives.inspect(sources) reads headers synchronously and returns
{kind, refusal}. kind names the leading bytes: rar, 7z, zip, tar,
tar.gz, tar.bz2, tar.xz, tar.zst, pkg (\x7FCNT), exfat, ffpkg (a
UFS2 superblock), ffpfs, ffpfsc, or ''. For an archive, refusal is the
decoder-memory message above when the headers read so far already declare a
window over 64 MiB: an xz/zstd first block, or a RAR's file headers. A RAR's
first file header is always read; the walk then stops at the first header it
reaches after about 100 ms (each sits after the previous file's data, so a
volume of many files may be cut short). An extraction refuses the rest: its
preflight reads every header, and in a streamed set, which is checked by
inspect too, the RAR worker refuses any later header over its limits. It
works on a download's growing .part file: a hole or a missing end is not an
error, so a first volume can be refused before the remaining volumes download.
7z and ZIP keep their directories at the end and show no refusal until complete.
An empty kind is not proof of anything; image kinds are recognized by superblock
magic only, not validated as mountable.
Browser download capture (ABI v7)
import {BrowserCapture} from '@ps5-react/core';
const task = BrowserCapture.capture({
prefix: 'https://vikingfile.com/d/',
suffix: '/Selected file.pkg',
timeoutSeconds: 180,
});
const off = task.subscribe(({state}) => console.log(state));
try { const {url} = await task.done; /* Validate with HTTP before transferring. */ }
finally { off(); }
PS5-only, one capture at a time. Sends bundled browser-capture.elf to the
localhost ELF loader on port 9021. The desktop returns a failed task explaining
that a PS5 is required. The only permitted prefix is the exact Vikingfile HTTPS
download host; suffix is / plus a filename of at most 160 UTF-8 bytes without
controls, slashes, backslashes or ... The deadline is an integer from 1 to
180 seconds. Snapshots have id, state, error and a matching url on
completion. cancel() shuts down the connection, stops the payload's next
read, and rejects done with AbortError. Host exit stops and joins the worker.
The user must complete the provider's verification in the system browser; the
verified link is captured as Vikingfile's page holds it (a JSON string, slashes
escaped), so the user need not select Download or its advertisements. This API does not bypass verification or open the page; the app
opens it with Linking.openURL after submitting capture. Only a uniquely
identified browser network process is read, with identity checked before every
read. Scanning is limited to anonymous readable/writable nonexecutable user
pages, 64 KiB every 8 ms, 1 GiB per session. No dumps, cookies, arbitrary
process selection or writes. Only complete ASCII/UTF-16 matching routes are
returned. Provenance: native/ps5/payloads/browser/README.md (Orbit Store 1.0.1 and 1.3.5).
A captured URL may already be stale; the app must validate host, decoded filename, HTTP status, content type and length before starting immediately. Local tests and compilation do not establish console/firmware support.
Native shape: browser.capture(prefix, suffix, timeoutSeconds) returns a task
ID; browser.cancel(id) requests cancellation; browser.poll() consumes
terminal results. Calls submit/poll synchronously on the render thread; all
payload transport runs on the worker.