Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 37 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@

Zero-copy parsing and generation of Nintendo Switch file formats.

The crate is `no_std` by default and opts into `std` through the `filesystem-support` feature, so the
same format definitions serve both host-side tooling and code running on the console.
Turns a byte buffer into a validated view of an NRO, NSO, NACP, NPDM, or RomFS image, and builds each
of those formats back out of its parts. Nothing is copied to read an image and nothing is written to
disk to produce one, so the same definitions serve a host-side packer and code running on the console.

## Layers

The crate is organized in three layers, each usable on its own:

- **`raw`** -- `#[repr(C)]` binary structure definitions backed by [`zerocopy`]. Direct field access,
no parsing overhead, always available.
- **`read`** -- Parsing wrappers over the raw structures. Validate magic numbers and sizes, and report
a typed error per format. Always available.
- **`write`** -- Builders that assemble a format and return the finished image as a byte buffer, so the
caller chooses where the artifact lands. Requires `filesystem-support`.
no parsing overhead, no allocator.
- **`read`** -- Parsing wrappers over the raw structures. Validate magic numbers and sizes once, then
hand out parts without further checks, and report a typed error per format. No allocator.
- **`write`** -- Builders that assemble a format and return the finished image as a byte buffer, so
the caller chooses where the artifact lands. Needs a heap; the builders that walk a directory need
a filesystem too.

A fourth module, `elf`, extracts the segments of a linked ELF binary and hands them to the NRO and
NSO builders.

## Formats

Expand All @@ -29,15 +34,12 @@ The crate is organized in three layers, each usable on its own:
| PFS0 | Partition filesystem archive | ✓ | | ✓ |
| MOD0 | Module header embedded in executables | ✓ | ✓ | |

## Features

| Feature | Description |
|----------------------|----------------------------------------------------------------------------|
| `filesystem-support` | Enables the `write` layer and `std`. Required by the other features. |
| `elf-parsing` | Derives NRO and NSO segments from a linked ELF binary (`elf` module). |
| `lz4-compression` | LZ4 compression and decompression of NSO segments. |
## What this crate does not do

No features are enabled by default; that configuration is `no_std`.
It does not sign, encrypt, or verify anything: an NPDM's ACID signature is stored and reproduced but
never checked, and an NSO's segment hashes are computed on write yet left to the caller on read. It
also does not decompress, because a reader that borrows its buffer has nowhere to put the expanded
bytes.

## Usage

Expand All @@ -46,30 +48,41 @@ No features are enabled by default; that configuration is `no_std`.
nx-object = { git = "https://github.com/nx-std/nx-object" }
```

The default build carries every format and the standard library. A consumer that wants less -- one
format, or a build with no allocator and no OS -- selects it through the crate's Cargo features,
which are documented on each entry in [`Cargo.toml`](Cargo.toml).

## Development

```bash
just check --all-targets --all-features # compile check
just clippy --all-targets --all-features # lint
just test --all-features # cargo nextest run (falls back to cargo test)
just check-unused-deps # cargo machete
just fmt # cargo +nightly fmt --all

# The no_std half, the way CI builds it
just check --no-default-features --target aarch64-unknown-none
just clippy --no-default-features --target aarch64-unknown-none
# The bare-metal half, the way CI builds it
just check --no-default-features --features all-formats,alloc --target aarch64-unknown-none
just clippy --no-default-features --features all-formats,alloc --target aarch64-unknown-none
```

## References

- [switchbrew NRO](https://switchbrew.org/wiki/NRO)
- [switchbrew NSO](https://switchbrew.org/wiki/NSO)
- [switchbrew KIP](https://switchbrew.org/wiki/KIP)
- [switchbrew NACP](https://switchbrew.org/wiki/NACP)
- [switchbrew NPDM](https://switchbrew.org/wiki/NPDM)
- [switchbrew RomFS](https://switchbrew.org/wiki/RomFS)
Every format the crate covers is documented on the [switchbrew] wiki, except RomFS, whose layout the
console inherits unchanged from the 3DS.

- [NRO](https://switchbrew.org/wiki/NRO)
- [NSO](https://switchbrew.org/wiki/NSO)
- [KIP1](https://switchbrew.org/wiki/KIP1)
- [NACP](https://switchbrew.org/wiki/NACP)
- [NPDM](https://switchbrew.org/wiki/NPDM)
- [PFS0](https://switchbrew.org/wiki/NCA#PFS0)
- [MOD](https://switchbrew.org/wiki/MOD)
- [RomFS](https://www.3dbrew.org/wiki/RomFS)

## License

MIT. See [LICENSE](LICENSE).

[`zerocopy`]: https://docs.rs/zerocopy
[switchbrew]: https://switchbrew.org/wiki/Main_Page
2 changes: 1 addition & 1 deletion src/blz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
//!
//! # References
//!
//! - <https://switchbrew.org/wiki/KIP>
//! - <https://switchbrew.org/wiki/KIP1>

use alloc::{vec, vec::Vec};

Expand Down
59 changes: 32 additions & 27 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,29 @@
//! Each layer is usable on its own, and each is a different answer to how much the caller wants
//! done for them.
//!
//! | Layer | What it gives you | Available |
//! |---------------|--------------------------------------------------------------|----------------------|
//! | [`mod@raw`] | The on-disk layouts as `#[repr(C)]` structures, unvalidated | always |
//! | [`mod@read`] | Views that validate once, then hand out parts without checks | always |
//! | [`mod@write`] | Builders returning a finished image as a byte buffer | `alloc` |
//! | [`mod@elf`] | Segments extracted from a linked ELF, ready for a builder | `elf-parsing` |
//! | Layer | What it gives you | Available |
//! |---------|--------------------------------------------------------------|---------------|
//! | `raw` | The on-disk layouts as `#[repr(C)]` structures, unvalidated | always |
//! | `read` | Views that validate once, then hand out parts without checks | always |
//! | `write` | Builders returning a finished image as a byte buffer | `alloc` |
//! | `elf` | Segments extracted from a linked ELF, ready for a builder | `elf-parsing` |
//!
//! Reach for [`mod@raw`] to inspect a field directly, and for [`mod@read`] to be told when the image
//! is not what it claims. A [`mod@read`] type that exists is one whose bounds have been proven,
//! which is why its accessors return plain slices rather than `Result`.
//! Reach for `raw` to inspect a field directly, and for `read` to be told when the image is not what
//! it claims. A `read` type that exists is one whose bounds have been proven, which is why its
//! accessors return plain slices rather than `Result`.
//!
//! The layers are named here rather than linked, because a build that leaves `write` or `elf` out
//! would have nothing for the link to resolve to and the table describes the crate whole.
//!
//! # `no_std`
//!
//! The crate is `no_std` unless `std` is enabled, and it needs an allocator only where the work
//! genuinely does. Three tiers, each a feature:
//!
//! - **Bare `no_std`** gives [`mod@raw`] and [`mod@read`]. Parsing borrows the buffer it is handed,
//! so it allocates nothing at all.
//! - **`alloc`** adds [`mod@write`]. A builder has to put the assembled bytes somewhere, and a heap
//! is the whole of what it needs.
//! - **Bare `no_std`** gives `raw` and `read`. Parsing borrows the buffer it is handed, so it
//! allocates nothing at all.
//! - **`alloc`** adds `write`. A builder has to put the assembled bytes somewhere, and a heap is the
//! whole of what it needs.
//! - **`std`** adds the `from_directory` builders and the path-carrying errors they report, which
//! are the only things here that touch a filesystem.
//!
Expand All @@ -38,16 +41,16 @@
//!
//! # Formats
//!
//! | Format | What it is | [`mod@raw`] | [`mod@read`] | [`mod@write`] |
//! |--------|-----------------------------------------------|:-----------:|:------------:|:-------------:|
//! | NRO | The executable the homebrew menu launches | ✓ | ✓ | ✓ |
//! | NSO | The executable format system modules use | ✓ | ✓ | ✓ |
//! | KIP | An initial process the kernel starts directly | ✓ | | ✓ |
//! | NACP | How a title is presented and what it may do | ✓ | ✓ | ✓ |
//! | NPDM | The permissions a program is granted | ✓ | ✓ | ✓ |
//! | RomFS | The read-only filesystem a title ships | ✓ | ✓ | ✓ |
//! | PFS0 | The flat archive an NSP is built from | ✓ | | ✓ |
//! | MOD0 | The runtime header embedded in an executable | ✓ | ✓ | |
//! | Format | What it is | `raw` | `read` | `write` |
//! |--------|-----------------------------------------------|:-----:|:------:|:-------:|
//! | NRO | The executable the homebrew menu launches | ✓ | ✓ | ✓ |
//! | NSO | The executable format system modules use | ✓ | ✓ | ✓ |
//! | KIP | An initial process the kernel starts directly | ✓ | | ✓ |
//! | NACP | How a title is presented and what it may do | ✓ | ✓ | ✓ |
//! | NPDM | The permissions a program is granted | ✓ | ✓ | ✓ |
//! | RomFS | The read-only filesystem a title ships | ✓ | ✓ | ✓ |
//! | PFS0 | The flat archive an NSP is built from | ✓ | | ✓ |
//! | MOD0 | The runtime header embedded in an executable | ✓ | ✓ | |
//!
//! # What this crate does not do
//!
Expand All @@ -59,15 +62,17 @@
//! # References
//!
//! Every format is documented on the switchbrew wiki, and each module links the page for the
//! structure it mirrors.
//! structure it mirrors. RomFS is the exception: switchbrew has no page for it, and the layout is
//! the one the console inherits unchanged from the 3DS.
//!
//! - [NRO](https://switchbrew.org/wiki/NRO)
//! - [NSO](https://switchbrew.org/wiki/NSO)
//! - [KIP](https://switchbrew.org/wiki/KIP)
//! - [KIP1](https://switchbrew.org/wiki/KIP1)
//! - [NACP](https://switchbrew.org/wiki/NACP)
//! - [NPDM](https://switchbrew.org/wiki/NPDM)
//! - [RomFS](https://switchbrew.org/wiki/RomFS)
//! - [PFS0](https://switchbrew.org/wiki/PFS0)
//! - [MOD](https://switchbrew.org/wiki/MOD)
//! - [PFS0](https://switchbrew.org/wiki/NCA#PFS0)
//! - [RomFS](https://www.3dbrew.org/wiki/RomFS)

#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
Expand Down
8 changes: 4 additions & 4 deletions src/raw/kip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub const KIP1_MAGIC: u32 = 0x3150494b;
/// stored length to find the compressed bytes and the final length to know how much room to leave.
/// When the segment's compression bit in [`Kip1Header::flags`] is clear, the two are equal.
///
/// See <https://switchbrew.org/wiki/KIP#Segment_Header>.
/// See <https://switchbrew.org/wiki/KIP1#Segment_Header>.
#[derive(Debug, Clone, Copy, zerocopy::FromZeros, zerocopy::IntoBytes, zerocopy::Immutable)]
#[repr(C)]
pub struct Kip1Segment {
Expand All @@ -36,7 +36,7 @@ pub struct Kip1Segment {
pub attributes: U32,
}

// Verify struct size - https://switchbrew.org/wiki/KIP#Segment_Header
// Verify struct size - https://switchbrew.org/wiki/KIP1#Segment_Header
const_assert_eq!(size_of::<Kip1Segment>(), 0x10);
const_assert_eq!(align_of::<Kip1Segment>(), 0x1);

Expand All @@ -46,7 +46,7 @@ const_assert_eq!(align_of::<Kip1Segment>(), 0x1);
/// Occupies the first `0x100` bytes of the file. A KIP is launched by the kernel before any
/// filesystem exists, so the header carries what a loader would otherwise read from an NPDM.
///
/// See <https://switchbrew.org/wiki/KIP#KIP_Header>.
/// See <https://switchbrew.org/wiki/KIP1#KIP1>.
#[derive(Debug, Clone, Copy, zerocopy::FromZeros, zerocopy::IntoBytes, zerocopy::Immutable)]
#[repr(C)]
pub struct Kip1Header {
Expand Down Expand Up @@ -79,6 +79,6 @@ pub struct Kip1Header {
pub capabilities: [u8; 0x80],
}

// Verify struct size - https://switchbrew.org/wiki/KIP#KIP_Header
// Verify struct size - https://switchbrew.org/wiki/KIP1#KIP1
const_assert_eq!(size_of::<Kip1Header>(), 0x100);
const_assert_eq!(align_of::<Kip1Header>(), 0x1);
4 changes: 2 additions & 2 deletions src/raw/mod0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub const MOD0_MAGIC: u32 = 0x30444f4d;
///
/// [`NroStart::mod_offset`]: crate::raw::nro::NroStart::mod_offset
///
/// See <https://switchbrew.org/wiki/NRO#MOD>.
/// See <https://switchbrew.org/wiki/MOD#ModuleHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct Mod0Header {
Expand All @@ -45,6 +45,6 @@ pub struct Mod0Header {
pub module_object_offset: I32,
}

// Verify struct size - https://switchbrew.org/wiki/NRO#MOD
// Verify struct size - https://switchbrew.org/wiki/MOD#ModuleHeader
const_assert_eq!(size_of::<Mod0Header>(), 0x1C);
const_assert_eq!(align_of::<Mod0Header>(), 0x1);
4 changes: 2 additions & 2 deletions src/raw/nacp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use zerocopy::{
/// indexing rather than by searching. An entry left zeroed means the title offers no name in that
/// language and the console falls back to another.
///
/// See <https://switchbrew.org/wiki/NACP#Title>.
/// See <https://switchbrew.org/wiki/NACP#ApplicationTitle>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NacpLanguageEntry {
Expand All @@ -29,7 +29,7 @@ pub struct NacpLanguageEntry {
pub author: [u8; 0x100],
}

// Verify struct size - https://switchbrew.org/wiki/NACP#Title
// Verify struct size - https://switchbrew.org/wiki/NACP#ApplicationTitle
const_assert_eq!(size_of::<NacpLanguageEntry>(), 0x300);
const_assert_eq!(align_of::<NacpLanguageEntry>(), 0x1);

Expand Down
8 changes: 4 additions & 4 deletions src/raw/npdm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const_assert_eq!(align_of::<NpdmHeader>(), 0x1);
/// signature is what makes that binding. ACI0 then requests some subset of it, and the loader
/// rejects a program asking for more than its ACID allows.
///
/// See <https://switchbrew.org/wiki/NPDM#ACID>.
/// See <https://switchbrew.org/wiki/NPDM#Acid>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct AcidHeader {
Expand Down Expand Up @@ -112,7 +112,7 @@ pub struct AcidHeader {
_reserved_238: U64,
}

// Verify struct size - https://switchbrew.org/wiki/NPDM#ACID
// Verify struct size - https://switchbrew.org/wiki/NPDM#Acid
const_assert_eq!(size_of::<AcidHeader>(), 0x240);
const_assert_eq!(align_of::<AcidHeader>(), 0x1);

Expand All @@ -122,7 +122,7 @@ const_assert_eq!(align_of::<AcidHeader>(), 0x1);
/// is checked against it at load, so nothing here grants anything the descriptor did not already
/// allow. Its blocks are laid out identically, which is what lets the loader compare them directly.
///
/// See <https://switchbrew.org/wiki/NPDM#ACI0>.
/// See <https://switchbrew.org/wiki/NPDM#Aci>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct Aci0Header {
Expand All @@ -147,6 +147,6 @@ pub struct Aci0Header {
_reserved_38: U64,
}

// Verify struct size - https://switchbrew.org/wiki/NPDM#ACI0
// Verify struct size - https://switchbrew.org/wiki/NPDM#Aci
const_assert_eq!(size_of::<Aci0Header>(), 0x40);
const_assert_eq!(align_of::<Aci0Header>(), 0x1);
12 changes: 6 additions & 6 deletions src/raw/nro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub const ASSET_MAGIC: u32 = 0x54455341;
/// The three segments of an NRO appear in [`NroHeader::segments`] in load order: `text`, `rodata`, `data`.
/// Each is mapped with its own permissions, so a segment's bounds decide which pages are executable.
///
/// See <https://switchbrew.org/wiki/NRO#Segments>.
/// See <https://switchbrew.org/wiki/NRO#NroHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NroSegment {
Expand All @@ -39,7 +39,7 @@ pub struct NroSegment {
pub size: U32,
}

// Verify struct size - https://switchbrew.org/wiki/NRO#Segments
// Verify struct size - https://switchbrew.org/wiki/NRO#NroHeader
const_assert_eq!(size_of::<NroSegment>(), 0x8);
const_assert_eq!(align_of::<NroSegment>(), 0x1);

Expand All @@ -49,7 +49,7 @@ const_assert_eq!(align_of::<NroSegment>(), 0x1);
/// A homebrew NRO puts its crt0 branch here; preserving those bytes is what keeps an NRO
/// launchable after a rewrite.
///
/// See <https://switchbrew.org/wiki/NRO#Start>.
/// See <https://switchbrew.org/wiki/NRO#RocrtHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NroStart {
Expand All @@ -63,15 +63,15 @@ pub struct NroStart {
_padding: [u8; 8],
}

// Verify struct size - https://switchbrew.org/wiki/NRO#Start
// Verify struct size - https://switchbrew.org/wiki/NRO#RocrtHeader
const_assert_eq!(size_of::<NroStart>(), 0x10);
const_assert_eq!(align_of::<NroStart>(), 0x1);

/// Everything the loader needs to map an NRO: its extent, its segments, and its identity.
///
/// Sits at offset `0x10`, immediately after [`NroStart`].
///
/// See <https://switchbrew.org/wiki/NRO#Header>.
/// See <https://switchbrew.org/wiki/NRO#NroHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NroHeader {
Expand Down Expand Up @@ -101,7 +101,7 @@ pub struct NroHeader {
_reserved2: [u8; 0x20],
}

// Verify struct size - https://switchbrew.org/wiki/NRO#Header
// Verify struct size - https://switchbrew.org/wiki/NRO#NroHeader
const_assert_eq!(size_of::<NroHeader>(), 0x70);
const_assert_eq!(align_of::<NroHeader>(), 0x1);

Expand Down
8 changes: 4 additions & 4 deletions src/raw/nso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ bitflags! {

/// Where one segment sits in the file, where it lands in memory, and how large it is once expanded.
///
/// See <https://switchbrew.org/wiki/NSO#Segment_Header>.
/// See <https://switchbrew.org/wiki/NSO#NsoHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NsoSegmentHeader {
Expand All @@ -58,7 +58,7 @@ pub struct NsoSegmentHeader {
pub size: U32,
}

// Verify struct size - https://switchbrew.org/wiki/NSO#Segment_Header
// Verify struct size - https://switchbrew.org/wiki/NSO#NsoHeader
const_assert_eq!(size_of::<NsoSegmentHeader>(), 0xC);
const_assert_eq!(align_of::<NsoSegmentHeader>(), 0x1);

Expand All @@ -67,7 +67,7 @@ const_assert_eq!(align_of::<NsoSegmentHeader>(), 0x1);
/// Occupies the first `0x100` bytes of the file. Unlike an NRO, an NSO opens with its magic rather
/// than with code, and its segments may be compressed and verified individually.
///
/// See <https://switchbrew.org/wiki/NSO#Header>.
/// See <https://switchbrew.org/wiki/NSO#NsoHeader>.
#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C)]
pub struct NsoHeader {
Expand Down Expand Up @@ -119,6 +119,6 @@ pub struct NsoHeader {
pub data_hash: [u8; 0x20],
}

// Verify struct size - https://switchbrew.org/wiki/NSO#Header
// Verify struct size - https://switchbrew.org/wiki/NSO#NsoHeader
const_assert_eq!(size_of::<NsoHeader>(), 0x100);
const_assert_eq!(align_of::<NsoHeader>(), 0x1);
Loading