├── .github └── workflows │ ├── audit.yml │ ├── test-and-publish-cli.yml │ └── test-and-publish-lib.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── README.md ├── assets ├── M1_Model.pack ├── M1_Player_MarioMdl.bfres ├── M3_Model.pack └── MW_Model.pack ├── crate_publish.sh ├── crate_version_exists.sh ├── ninres-cli ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── ninres ├── .vscode │ └── settings.json ├── Cargo.toml ├── examples │ ├── sarc.rs │ └── web │ │ ├── .babelrc │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── package.json │ │ ├── src │ │ ├── app.tsx │ │ ├── badge.tsx │ │ ├── header.tsx │ │ ├── images │ │ │ └── icon.png │ │ ├── index.html │ │ ├── index.tsx │ │ ├── ninres.tsx │ │ ├── smmdb.ts │ │ └── types │ │ │ └── image.d.ts │ │ ├── tsconfig.json │ │ ├── webpack.config.js │ │ ├── webpack.dev.js │ │ ├── webpack.prod.js │ │ └── yarn.lock └── src │ ├── bfres.rs │ ├── bntx.rs │ ├── bntx │ └── util.rs │ ├── bom.rs │ ├── error.rs │ ├── lib.rs │ └── sarc.rs └── package.json /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths: 4 | - "**/Cargo.toml" 5 | - "**/Cargo.lock" 6 | pull_request: 7 | paths: 8 | - "**/Cargo.toml" 9 | - "**/Cargo.lock" 10 | 11 | name: Continuous Integration Audit 12 | 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | rust: 20 | - stable 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | name: Checkout 25 | 26 | - uses: actions-rs/toolchain@v1 27 | name: Install Toolchain 28 | with: 29 | profile: minimal 30 | toolchain: ${{ matrix.rust }} 31 | override: true 32 | 33 | - uses: actions-rs/cargo@v1 34 | name: Install Cargo Tools 35 | with: 36 | command: install 37 | args: cargo-audit 38 | 39 | - uses: actions-rs/cargo@v1 40 | name: Audit 41 | with: 42 | command: audit 43 | -------------------------------------------------------------------------------- /.github/workflows/test-and-publish-cli.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous Integration Cli 4 | 5 | env: 6 | CRATE_PATH: ninres-cli 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | continue-on-error: ${{ matrix.experimental }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | rust: 16 | - stable 17 | - beta 18 | - 1.53.0 19 | experimental: [false] 20 | include: 21 | - rust: nightly 22 | experimental: true 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | name: Checkout 27 | 28 | - uses: actions-rs/toolchain@v1 29 | name: Install Toolchain 30 | with: 31 | profile: minimal 32 | toolchain: ${{ matrix.rust }} 33 | override: true 34 | components: rustfmt, clippy 35 | 36 | - uses: actions-rs/cargo@v1 37 | name: Check 38 | with: 39 | command: check 40 | args: -p ${{ env.CRATE_PATH }} 41 | 42 | - uses: actions-rs/cargo@v1 43 | name: Test 44 | with: 45 | command: test 46 | args: -p ${{ env.CRATE_PATH }} 47 | 48 | - uses: actions-rs/cargo@v1 49 | name: Fmt 50 | with: 51 | command: fmt 52 | args: -p ${{ env.CRATE_PATH }} -- --check 53 | 54 | - uses: actions-rs/cargo@v1 55 | name: Clippy 56 | with: 57 | command: clippy 58 | args: -p ${{ env.CRATE_PATH }} -- -D warnings 59 | 60 | publish: 61 | if: github.ref == 'refs/heads/master' 62 | needs: test 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v2 66 | name: Checkout 67 | 68 | - uses: actions-rs/toolchain@v1 69 | name: Install Toolchain 70 | with: 71 | profile: minimal 72 | toolchain: beta 73 | override: true 74 | 75 | - name: Check version 76 | run: | 77 | cargo install cargo-whatfeatures --no-default-features --features "rustls" 78 | export LIB_VERSION=$(cat $CRATE_PATH/Cargo.toml | grep version | head -1 | sed 's/[",(version = )]//g') 79 | echo LIB_VERSION=$LIB_VERSION 80 | export CRATE_VERSION_EXISTS=$(NO_COLOR=1 ./crate_version_exists.sh $LIB_VERSION $CRATE_PATH) 81 | echo CRATE_VERSION_EXISTS=$CRATE_VERSION_EXISTS 82 | 83 | - name: Deploy to Crates.io 84 | env: 85 | CARGO_CREDENTIALS: ${{ secrets.CARGO_CREDENTIALS }} 86 | run: bash crate_publish.sh $CRATE_PATH 87 | continue-on-error: true 88 | -------------------------------------------------------------------------------- /.github/workflows/test-and-publish-lib.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous Integration Lib 4 | 5 | env: 6 | CRATE_PATH: ninres 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | continue-on-error: ${{ matrix.experimental }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | rust: 16 | - stable 17 | - beta 18 | - 1.53.0 19 | experimental: [false] 20 | include: 21 | - rust: nightly 22 | experimental: true 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | name: Checkout 27 | 28 | - uses: actions-rs/toolchain@v1 29 | name: Install Toolchain 30 | with: 31 | profile: minimal 32 | toolchain: ${{ matrix.rust }} 33 | override: true 34 | components: rustfmt, clippy 35 | 36 | - uses: actions-rs/cargo@v1 37 | name: Check 38 | with: 39 | command: check 40 | args: -p ${{ env.CRATE_PATH }} 41 | 42 | - uses: actions-rs/cargo@v1 43 | name: Test 44 | with: 45 | command: test 46 | args: -p ${{ env.CRATE_PATH }} 47 | 48 | - uses: actions-rs/cargo@v1 49 | name: Fmt 50 | with: 51 | command: fmt 52 | args: -p ${{ env.CRATE_PATH }} -- --check 53 | 54 | - uses: actions-rs/cargo@v1 55 | name: Clippy 56 | with: 57 | command: clippy 58 | args: -p ${{ env.CRATE_PATH }} -- -D warnings 59 | 60 | publish: 61 | if: github.ref == 'refs/heads/master' 62 | needs: test 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v2 66 | name: Checkout 67 | 68 | - uses: actions-rs/toolchain@v1 69 | name: Install Toolchain 70 | with: 71 | profile: minimal 72 | toolchain: beta 73 | override: true 74 | 75 | - name: Set up git 76 | run: | 77 | git config --global user.email "github@actions.com" 78 | git config --global user.name "GitHub Actions" 79 | cp README.md $CRATE_PATH/README.md 80 | git add $CRATE_PATH/README.md 81 | git commit -m "add readme" 82 | 83 | - name: Check version 84 | run: | 85 | cargo install cargo-whatfeatures --no-default-features --features "rustls" 86 | export LIB_VERSION=$(cat $CRATE_PATH/Cargo.toml | grep version | head -1 | sed 's/[",(version = )]//g') 87 | echo LIB_VERSION=$LIB_VERSION 88 | export CRATE_VERSION_EXISTS=$(NO_COLOR=1 ./crate_version_exists.sh $LIB_VERSION $CRATE_PATH) 89 | echo CRATE_VERSION_EXISTS=$CRATE_VERSION_EXISTS 90 | 91 | - name: Deploy to Crates.io 92 | env: 93 | CARGO_CREDENTIALS: ${{ secrets.CARGO_CREDENTIALS }} 94 | run: bash crate_publish.sh $CRATE_PATH 95 | continue-on-error: true 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.tar 3 | .idea 4 | Cargo.lock 5 | 6 | **/assets/extracted/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "rust-analyzer.cargo.allFeatures": true, 4 | "rust-analyzer.diagnostics.disabled": ["macro-error", "missing-unsafe"] 5 | // "rust-analyzer.cargo.target": "wasm32-unknown-unknown" 6 | } 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ninres", 4 | "ninres-cli" 5 | ] 6 | 7 | [profile] 8 | [profile.release] 9 | lto = "fat" 10 | codegen-units = 1 11 | 12 | [profile.dev] 13 | opt-level = 1 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ninres-rs 2 | 3 | ![Continuous integration](https://github.com/Tarnadas/ninres-rs/workflows/Continuous%20integration/badge.svg) 4 | [blog.rust-lang.org](https://blog.rust-lang.org/2021/06/17/Rust-1.53.0.html) 5 | [![Discord](https://img.shields.io/discord/168893527357521920?label=Discord&logo=discord&color=7289da)](https://discord.gg/SPZsgSe) 6 | 7 | Read commonly used Nintendo file formats. 8 | 9 | Please refer to the Wiki: 10 | https://github.com/Kinnay/Nintendo-File-Formats/wiki 11 | 12 | All file formats are behind feature flags. 13 | Here is a list of available Nintendo file format features: 14 | 15 | `bfres`, `sarc` 16 | 17 | You can also enable additional features: 18 | 19 | `tar`: write Nintendo resource to tar ball. 20 | 21 | `zstd`: ZSTD decompression. 22 | 23 | `png`: allows extracting textures as png. 24 | 25 | The library is written in Rust and compiles to WebAssembly for the web or can be used as a standard Rust Crate. 26 | A live demo running in your browser can be found here: 27 | https://tarnadas.github.io/ninres-rs/ 28 | 29 | ### Examples 30 | 31 | Enable desired features in `Cargo.toml`. 32 | 33 | ```toml 34 | [dependencies] 35 | ninres = { version = "*", features = ["bfres", "sarc", "zstd"] } 36 | ``` 37 | 38 | In your `main.rs`. 39 | 40 | ```rust 41 | use std::fs::read; 42 | use ninres::{NinRes, NinResFile}; 43 | 44 | let buffer = read("foo.pack")?; 45 | let ninres = buffer.as_ninres()?; 46 | 47 | match &ninres { 48 | NinResFile::Bfres(_bfres) => {} 49 | NinResFile::Sarc(_sarc) => {} 50 | } 51 | ``` 52 | 53 | ## Write to tar 54 | 55 | Convert resource into tar buffer. 56 | This buffer can then e.g. be stored in a file. 57 | 58 | The `mode` parameter refers to the file mode within the tar ball. 59 | 60 | ### Examples 61 | 62 | ```rust 63 | use ninres::{sarc::Sarc, IntoTar}; 64 | use std::{fs::{read, File}, io::Write}; 65 | 66 | let sarc_file = Sarc::new(&read("./assets/M1_Model.pack")?)?; 67 | let tar = sarc_file.into_tar(0o644)?; 68 | 69 | let mut file = File::create("M1_Model.tar")?; 70 | file.write_all(&tar.into_inner()[..])?; 71 | ``` 72 | -------------------------------------------------------------------------------- /assets/M1_Model.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/ninres-rs/2234bc2cabd86b93d491568363cf215171a356dc/assets/M1_Model.pack -------------------------------------------------------------------------------- /assets/M1_Player_MarioMdl.bfres: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/ninres-rs/2234bc2cabd86b93d491568363cf215171a356dc/assets/M1_Player_MarioMdl.bfres -------------------------------------------------------------------------------- /assets/M3_Model.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/ninres-rs/2234bc2cabd86b93d491568363cf215171a356dc/assets/M3_Model.pack -------------------------------------------------------------------------------- /assets/MW_Model.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/ninres-rs/2234bc2cabd86b93d491568363cf215171a356dc/assets/MW_Model.pack -------------------------------------------------------------------------------- /crate_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $CRATE_VERSION_EXISTS == 1 ]]; then 4 | exit 0 5 | fi 6 | 7 | args=("$@") 8 | CRATE_PATH=${args[0]} 9 | 10 | # Crates.io publish 11 | cd $CRATE_PATH && cargo publish --token $CARGO_CREDENTIALS 12 | -------------------------------------------------------------------------------- /crate_version_exists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | { 4 | args=("$@") 5 | LIB_VERSION=${args[0]} 6 | CRATE_PATH=${args[1]} 7 | 8 | VERSIONS=$(cd $CRATE_PATH && cargo whatfeatures -l $CRATE_PATH) 9 | 10 | VERION_EXISTS=0 11 | 12 | while IFS=$'\n' read -ra VERSION; do 13 | for V in "${VERSION[@]}"; do 14 | V=$(cut -d '#' -f 1 <<< "$V") 15 | V=$(sed 's/[",($CRATE_PATH = )]//g' <<< $V) 16 | if [ "$LIB_VERSION" = "$V" ]; then 17 | VERION_EXISTS=1 18 | break 19 | fi 20 | done 21 | done <<< "$VERSIONS" 22 | } >/dev/null 2>&1 23 | 24 | echo $VERION_EXISTS 25 | -------------------------------------------------------------------------------- /ninres-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ninres-cli" 3 | version = "0.0.1" 4 | description = "Read commonly used Nintendo file formats." 5 | authors = ["Mario Reder "] 6 | license = "MIT OR Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/tarnadas/ninres-rs" 9 | readme = "README.md" 10 | keywords = ["gamedev", "parsing", "wasm"] 11 | categories = ["game-development", "parser-implementations", "wasm"] 12 | 13 | [[bin]] 14 | name = "ninres" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | color-eyre = "0.5" 19 | image = { version = "0.23", default-features = false, features = ["png"] } 20 | ninres = { version = "0.0", path = "../ninres", features = ["bfres", "sarc", "tar", "zstd"] } 21 | structopt = "0.3" 22 | 23 | [profile.dev.package.backtrace] 24 | opt-level = 3 25 | -------------------------------------------------------------------------------- /ninres-cli/README.md: -------------------------------------------------------------------------------- 1 | # Ninres-cli 2 | 3 | A command-line tool to handle commonly used Nintendo file formats. 4 | 5 | Please refer to the [Wiki](https://github.com/Kinnay/Nintendo-File-Formats/wiki). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | cargo install ninres-cli 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```bash 16 | ninres [SUBCOMMAND] 17 | 18 | FLAGS: 19 | -h, --help Prints help information 20 | -V, --version Prints version information 21 | 22 | SUBCOMMANDS: 23 | extract Extract assets from given input file 24 | help Prints this message or the help of the given subcommand(s) 25 | ``` 26 | 27 | ```bash 28 | ninres extract --input --output 29 | 30 | FLAGS: 31 | -h, --help Prints help information 32 | -V, --version Prints version information 33 | 34 | OPTIONS: 35 | -i, --input 36 | -o, --output 37 | ``` 38 | -------------------------------------------------------------------------------- /ninres-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use image::{DynamicImage, ImageBuffer}; 3 | use ninres::{Bfres, EmbeddedFile, NinRes, NinResFile, Sarc}; 4 | use std::{ 5 | cmp, 6 | fs::{self, read}, 7 | path::PathBuf, 8 | }; 9 | use structopt::StructOpt; 10 | 11 | /// A command-line tool to handle commonly used Nintendo files formats. 12 | #[derive(StructOpt, Debug)] 13 | #[structopt(name = "ninres")] 14 | struct Opt { 15 | #[structopt(subcommand)] 16 | pub cmd: Option, 17 | } 18 | 19 | #[derive(StructOpt, Debug, PartialEq)] 20 | pub enum Cmd { 21 | /// Extract assets from given input file 22 | Extract(ExtractOpt), 23 | } 24 | 25 | #[derive(StructOpt, Debug, PartialEq)] 26 | pub struct ExtractOpt { 27 | #[structopt(short, long, parse(from_os_str))] 28 | pub input: PathBuf, 29 | #[structopt(short, long, parse(from_os_str))] 30 | pub output: PathBuf, 31 | } 32 | 33 | fn main() -> Result<()> { 34 | color_eyre::install()?; 35 | 36 | let opt = Opt::from_args(); 37 | 38 | match opt.cmd { 39 | Some(Cmd::Extract(extract_options)) => { 40 | let buffer = read(extract_options.input)?; 41 | let ninres = buffer.as_ninres()?; 42 | 43 | match &ninres { 44 | NinResFile::Bfres(bfres) => { 45 | extract_bfres(bfres, extract_options.output)?; 46 | } 47 | NinResFile::Sarc(sarc) => { 48 | extract_sarc(sarc, extract_options.output)?; 49 | } 50 | } 51 | } 52 | None => { 53 | Opt::clap().print_help()?; 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn extract_bfres(bfres: &Bfres, out_path: PathBuf) -> Result<()> { 61 | for file in bfres.get_embedded_files().iter() { 62 | match file { 63 | EmbeddedFile::BNTX(bntx) => { 64 | for texture in bntx.get_textures().iter() { 65 | for (tex_count, mips) in texture.get_texture_data().iter().enumerate() { 66 | for (mip_level, mip) in mips.iter().enumerate() { 67 | let width = cmp::max(1, texture.width >> mip_level); 68 | let height = cmp::max(1, texture.height >> mip_level); 69 | let buf = if let Some(image) = 70 | ImageBuffer::from_raw(width, height, mip.clone()) 71 | { 72 | image 73 | } else { 74 | // TODO ? 75 | continue; 76 | }; 77 | let image = DynamicImage::ImageRgba8(buf); 78 | 79 | let mut path = out_path.clone(); 80 | if !path.exists() { 81 | fs::create_dir(path.clone())?; 82 | } 83 | path.push(&format!( 84 | "{}_{}_{}.png", 85 | texture.get_name(), 86 | tex_count, 87 | mip_level 88 | )); 89 | if let Err(_err) = image.save(&path) { 90 | // TODO 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | Ok(()) 99 | } 100 | 101 | fn extract_sarc(sarc: &Sarc, out_path: PathBuf) -> Result<()> { 102 | sarc.get_sfat_nodes() 103 | .iter() 104 | .map(move |sfat| -> Result<_> { 105 | let mut path = out_path.clone(); 106 | if let Some(sfat_path) = sfat.get_path() { 107 | path.push(sfat_path); 108 | let mut folder_path = path.clone(); 109 | folder_path.pop(); 110 | if !folder_path.exists() { 111 | fs::create_dir_all(folder_path)?; 112 | } 113 | 114 | let data = if let Some(data) = sfat.get_data_decompressed() { 115 | data 116 | } else { 117 | sfat.get_data() 118 | }; 119 | 120 | if let Ok(file) = data.as_ninres() { 121 | path.set_extension(file.get_extension().to_string()); 122 | match &file { 123 | NinResFile::Bfres(bfres) => { 124 | let mut base_path = path.clone(); 125 | base_path.pop(); 126 | base_path.push(path.file_stem().unwrap()); 127 | extract_bfres(bfres, base_path)?; 128 | } 129 | NinResFile::Sarc(sarc) => { 130 | let mut base_path = path.clone(); 131 | base_path.pop(); 132 | base_path.push(path.file_stem().unwrap()); 133 | extract_sarc(sarc, base_path)?; 134 | } 135 | } 136 | } 137 | fs::write(path, data)?; 138 | } 139 | Ok(()) 140 | }) 141 | .collect::>>()?; 142 | Ok(()) 143 | } 144 | -------------------------------------------------------------------------------- /ninres/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.extraArgs": ["--features=tar,zstd"], 3 | "rust-analyzer.cargo.features": ["bfres", "sarc", "tar", "zstd"] 4 | } 5 | -------------------------------------------------------------------------------- /ninres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ninres" 3 | version = "0.0.2" 4 | description = "Read commonly used Nintendo file formats." 5 | authors = ["Mario Reder "] 6 | license = "MIT OR Apache-2.0" 7 | edition = "2018" 8 | repository = "https://github.com/tarnadas/ninres-rs" 9 | readme = "README.md" 10 | keywords = ["gamedev", "parsing", "wasm"] 11 | categories = ["game-development", "parser-implementations", "wasm"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "rlib"] 15 | name = "ninres" 16 | path = "src/lib.rs" 17 | 18 | [dependencies] 19 | byteorder = "1" 20 | cfg-if = "1" 21 | derivative = { version = "2", features = ["use_core"], optional = true } 22 | image = { version = "0.24", default-features = false, optional = true } 23 | once_cell = "1" 24 | ruzstd = { version = "0.2", optional = true } 25 | tar_crate = { package = "tar", version = "0.4", optional = true } 26 | thiserror = "1" 27 | 28 | [target.'cfg(target_arch = "wasm32")'.dependencies] 29 | console_error_panic_hook = "0.1" 30 | js-sys = "0.3" 31 | wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } 32 | web-sys = { version = "0.3", features = ["console"] } 33 | wee_alloc = "0.4" 34 | 35 | [dev-dependencies] 36 | anyhow = "1" 37 | test-case = "1" 38 | 39 | [features] 40 | default = [] 41 | bfres = ["derivative"] 42 | sarc = [] 43 | tar = ["tar_crate"] 44 | zstd = ["ruzstd"] 45 | png = ["image", "image/png"] 46 | 47 | [package.metadata.docs.rs] 48 | all-features = true 49 | 50 | [[example]] 51 | name = "sarc" 52 | required-features = ["sarc", "zstd"] 53 | -------------------------------------------------------------------------------- /ninres/examples/sarc.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ninres::{NinRes, NinResFile, Sarc}; 3 | use std::{fs, path::PathBuf}; 4 | 5 | static M1_MODEL_PACK: &[u8] = include_bytes!("../../assets/M1_Model.pack"); 6 | 7 | fn main() -> Result<()> { 8 | let sarc = Sarc::new(M1_MODEL_PACK); 9 | extract_sarc(sarc.unwrap(), "assets/extracted".into()) 10 | } 11 | 12 | fn extract_sarc(sarc: Sarc, path: PathBuf) -> Result<()> { 13 | sarc.get_sfat_nodes() 14 | .iter() 15 | .map(move |sfat| -> Result<_> { 16 | let mut path = path.clone(); 17 | if let Some(sfat_path) = sfat.get_path() { 18 | path.push(sfat_path); 19 | let mut folder_path = path.clone(); 20 | folder_path.pop(); 21 | if !folder_path.exists() { 22 | fs::create_dir_all(folder_path)?; 23 | } 24 | 25 | let data = if let Some(data) = sfat.get_data_decompressed() { 26 | data 27 | } else { 28 | sfat.get_data() 29 | }; 30 | 31 | if let Ok(file) = data.as_ninres() { 32 | path.set_extension(file.get_extension().to_string()); 33 | if let NinResFile::Sarc(sarc) = file { 34 | let mut base_path = path.clone(); 35 | base_path.pop(); 36 | base_path.push(path.file_stem().unwrap()); 37 | extract_sarc(sarc, base_path)?; 38 | } 39 | } 40 | fs::write(path, data)?; 41 | } 42 | Ok(()) 43 | }) 44 | .collect::>>()?; 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /ninres/examples/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "edge >= 17", 9 | "ff >= 61", 10 | "chrome >= 63", 11 | "safari >= 11.1" 12 | ] 13 | }, 14 | "useBuiltIns": "usage", 15 | "modules": false, 16 | "corejs": 3 17 | } 18 | ] 19 | ], 20 | "plugins": [ 21 | [ 22 | "@babel/plugin-transform-typescript", 23 | { 24 | "isTSX": true 25 | } 26 | ], 27 | "@babel/plugin-transform-react-jsx", 28 | "@babel/plugin-syntax-dynamic-import" 29 | ] 30 | } -------------------------------------------------------------------------------- /ninres/examples/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:ordered-imports/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:react/recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "plugins": ["@typescript-eslint", "prettier", "react"], 12 | "parserOptions": { 13 | "ecmaVersion": 2020, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "arrowFunctions": true 18 | } 19 | }, 20 | "env": { 21 | "browser": true, 22 | "es6": true, 23 | "amd": true 24 | }, 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | }, 30 | "rules": { 31 | "react/prop-types": "off" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ninres/examples/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle-report.html 3 | stats.json 4 | dist 5 | -------------------------------------------------------------------------------- /ninres/examples/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /ninres/examples/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Mario Reder ", 3 | "name": "ninres-example", 4 | "version": "1.0.0", 5 | "description": "Ninres example.", 6 | "repository": "https://github.com/Tarnadas/ninres-rs", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "yarn clean && yarn --cwd ../../.. build:web-dev && run-p serve watch", 10 | "serve": "webpack-dev-server -d --env=dev", 11 | "build": "yarn clean && yarn build:wasm && webpack --env=prod", 12 | "build:wasm": "yarn --cwd ../../.. build:web", 13 | "watch": "yarn --cwd ../../.. watch", 14 | "deploy": "gh-pages --dist dist", 15 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 16 | "clean": "rimraf ./dist && rimraf ../../../pkg" 17 | }, 18 | "dependencies": { 19 | "@geist-ui/react": "^2", 20 | "react": "^17", 21 | "react-dom": "^17" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7", 25 | "@babel/plugin-syntax-dynamic-import": "^7", 26 | "@babel/plugin-transform-react-jsx": "^7", 27 | "@babel/plugin-transform-typescript": "^7", 28 | "@babel/polyfill": "^7", 29 | "@babel/preset-env": "^7", 30 | "@types/react": "^17", 31 | "@types/react-dom": "^17", 32 | "@typescript-eslint/eslint-plugin": "^4", 33 | "@typescript-eslint/parser": "^4", 34 | "babel-loader": "^8", 35 | "core-js": "3", 36 | "eslint": "^7", 37 | "eslint-config-prettier": "^8.3.0", 38 | "eslint-plugin-ordered-imports": "^0", 39 | "eslint-plugin-prettier": "^3", 40 | "eslint-plugin-react": "^7", 41 | "file-loader": "^6.2.0", 42 | "gh-pages": "^3", 43 | "html-webpack-plugin": "^5", 44 | "npm-run-all": "^4", 45 | "prettier": "^2", 46 | "ts-node": "^10", 47 | "typescript": "4.3", 48 | "webpack": "^5", 49 | "webpack-bundle-analyzer": "^4", 50 | "webpack-cli": "^3", 51 | "webpack-dev-server": "^3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ninres/examples/web/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | 3 | import { Button, Page, Spacer, Text } from '@geist-ui/react'; 4 | 5 | import { NinResFileExt } from '../../../pkg/ninres'; 6 | 7 | import { parseFile } from './smmdb'; 8 | import { Header } from './header'; 9 | import { Ninres } from './ninres'; 10 | 11 | export const App: FC = () => { 12 | const [ninresFile, setNinresFile] = useState(null); 13 | const [loading, setLoading] = useState(false); 14 | let upload: HTMLInputElement | null = null; 15 | 16 | const handleSelect = async (event: React.ChangeEvent) => { 17 | if (!event.target.files) return; 18 | const file = event.target.files[0]; 19 | if (!file) return; 20 | setLoading(true); 21 | try { 22 | const files = await parseFile(file); 23 | setNinresFile(files); 24 | } catch (err) { 25 | console.error(err); 26 | } 27 | setLoading(false); 28 | }; 29 | 30 | return ( 31 | <> 32 | 33 |
34 | 35 | Please select a resource file (SARC/BFRES). Currently only Super Mario 36 | Maker 2 resources have been tested. 37 | 38 | 49 | (upload = ref)} 54 | style={{ display: 'none' }} 55 | onChange={handleSelect} 56 | /> 57 | 58 | 59 | {ninresFile && } 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /ninres/examples/web/src/badge.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Grid } from '@geist-ui/react'; 4 | 5 | export const Badge: FC<{ 6 | data: string; 7 | }> = ({ data }) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /ninres/examples/web/src/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Display, Grid, Image, Text } from '@geist-ui/react'; 4 | 5 | import icon from './images/icon.png'; 6 | import { Badge } from './badge'; 7 | 8 | export const Header: FC = () => { 9 | const baseUrl = 'https://img.shields.io'; 10 | const starsUrl = `${baseUrl}/github/stars/tarnadas/ninres-rs?style=for-the-badge&logo=github&link=https://github.com/Tarnadas/ninres-rs&link=https://github.com/Tarnadas/ninres-rs/stargazers`; 11 | const cratesUrl = `${baseUrl}/crates/v/ninres?style=for-the-badge&logo=rust&link=https://crates.io/crates/ninres`; 12 | const discordUrl = `${baseUrl}/discord/168893527357521920?logo=discord&color=7289da&style=for-the-badge&link=https://discord.gg/SPZsgSe`; 13 | const twitterUrl = `${baseUrl}/twitter/follow/marior_dev?logo=twitter&label=follow&color=00acee&style=for-the-badge&link=https://twitter.com/marior_dev`; 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 |
28 | 29 | Ninres library 30 |
31 |
32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /ninres/examples/web/src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tarnadas/ninres-rs/2234bc2cabd86b93d491568363cf215171a356dc/ninres/examples/web/src/images/icon.png -------------------------------------------------------------------------------- /ninres/examples/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ninres example 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /ninres/examples/web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { CssBaseline, GeistProvider } from '@geist-ui/react'; 5 | 6 | import { App } from './app'; 7 | 8 | export let ninres: typeof import('../../../pkg/ninres'); 9 | 10 | (async () => { 11 | ninres = await import('../../../pkg/ninres'); 12 | ninres.setupPanicHook(); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ); 21 | })(); 22 | -------------------------------------------------------------------------------- /ninres/examples/web/src/ninres.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | 3 | import { Button, Card, Grid, Text } from '@geist-ui/react'; 4 | 5 | import { BNTX, NinResFileExt, SfatNode, Texture } from '../../../pkg/ninres'; 6 | 7 | import { parseData } from './smmdb'; 8 | 9 | export const Ninres: FC<{ ninres: NinResFileExt }> = ({ ninres }) => { 10 | const [ninresFileMap, setNinresFileMap] = useState<{ 11 | [key: number]: NinResFileExt; 12 | }>({}); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleExtract = (node: SfatNode) => async () => { 16 | if (loading) return; 17 | setLoading(true); 18 | await new Promise(resolve => setTimeout(resolve)); 19 | try { 20 | const { hash } = node; 21 | const files = await parseData(node.intoData()); 22 | setNinresFileMap({ 23 | [hash]: files, 24 | ...ninresFileMap 25 | }); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | setLoading(false); 30 | }; 31 | 32 | const getImageFromBinary = (data: Uint8Array) => { 33 | const blob = new Blob([data], { type: 'image/png' }); 34 | return URL.createObjectURL(blob); 35 | }; 36 | 37 | return ( 38 | <> 39 | 40 | {ninres 41 | .getSarc() 42 | ?.intoSfatNodes() 43 | .map((node: SfatNode) => { 44 | const nextFiles = ninresFileMap[node.hash]; 45 | const path = node.getPath(); 46 | const canExtract = 47 | !nextFiles && 48 | path && 49 | (path.includes('.zs') || path.includes('.bfres')); 50 | return ( 51 | 52 | 53 | 54 | {path} 55 | 56 | 57 | {canExtract ? ( 58 | 66 | ) : ( 67 | nextFiles && 68 | )} 69 | 70 | 71 | ); 72 | })} 73 | 74 | {ninres 75 | .getBfres() 76 | ?.intoBntxFiles() 77 | .reduce((arr: JSX.Element[], bntx: BNTX) => { 78 | bntx.getTextures().forEach((texture: Texture) => { 79 | const png = texture.asPng(0, 0); 80 | arr.push( 81 | 82 | 83 | 84 | {texture.getName()} 85 | 86 | 87 | {png && ( 88 | 92 | )} 93 | 94 | 95 | ); 96 | }); 97 | return arr; 98 | }, [])} 99 | 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /ninres/examples/web/src/smmdb.ts: -------------------------------------------------------------------------------- 1 | import { NinResFile, NinResFileExt } from '../../../pkg/ninres'; 2 | 3 | import { ninres } from '.'; 4 | 5 | export async function parseFile(file: File): Promise { 6 | const buffer = await readFile(file); 7 | return parseData(new Uint8Array(buffer)); 8 | } 9 | 10 | export async function parseData(buffer: Uint8Array): Promise { 11 | console.log('Processing file...'); 12 | const ninresFile = ninres.NinResFileExt.fromBytes(buffer); 13 | console.log('Processing complete'); 14 | switch (ninresFile.getFileType()) { 15 | case NinResFile.Sarc: 16 | ninresFile.getSarc()?.getSfatNodes(); 17 | } 18 | return ninresFile; 19 | } 20 | 21 | async function readFile(file: File): Promise { 22 | return new Promise(resolve => { 23 | const reader = new FileReader(); 24 | reader.addEventListener('loadend', () => { 25 | resolve(reader.result as ArrayBuffer); 26 | }); 27 | reader.readAsArrayBuffer(file); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /ninres/examples/web/src/types/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpeg'; 2 | declare module '*.jpg'; 3 | declare module '*.png'; 4 | -------------------------------------------------------------------------------- /ninres/examples/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "strict": true, 8 | "lib": ["esnext", "dom"], 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "allowSyntheticDefaultImports": true, 12 | "jsx": "preserve", 13 | "typeRoots": ["node_modules/@types", "src/types"] 14 | }, 15 | "include": ["src/**/*"], 16 | "ordered-imports": [ 17 | true, 18 | { 19 | "grouped-imports": true 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /ninres/examples/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function (env) { 4 | return require(`./webpack.${env}.js`); 5 | }; 6 | -------------------------------------------------------------------------------- /ninres/examples/web/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | 7 | const dist = path.resolve(__dirname, 'dist'); 8 | 9 | module.exports = { 10 | mode: 'development', 11 | entry: './src/index.tsx', 12 | output: { 13 | path: dist, 14 | filename: 'bundle.js' 15 | }, 16 | devtool: 'inline-source-map', 17 | devServer: { 18 | contentBase: dist 19 | }, 20 | plugins: [ 21 | new HtmlWebpackPlugin({ 22 | template: 'src/index.html' 23 | }), 24 | new webpack.EnvironmentPlugin({ 25 | NODE_ENV: 'development' 26 | }) 27 | ], 28 | resolve: { 29 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.wasm'] 30 | }, 31 | experiments: { 32 | asyncWebAssembly: true 33 | }, 34 | watchOptions: { 35 | aggregateTimeout: 200, 36 | ignored: ['../../target/**'] 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.tsx?$/, 42 | exclude: /node_modules/, 43 | use: ['babel-loader'] 44 | }, 45 | { 46 | test: /\.(jpe?g|png|gif|svg)$/i, 47 | use: 'file-loader' 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /ninres/examples/web/webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | 7 | const dist = path.resolve(__dirname, 'dist'); 8 | 9 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 10 | .BundleAnalyzerPlugin; 11 | 12 | module.exports = { 13 | mode: 'production', 14 | entry: './src/index.tsx', 15 | output: { 16 | path: dist, 17 | filename: 'bundle.js' 18 | }, 19 | devtool: 'source-map', 20 | plugins: [ 21 | new HtmlWebpackPlugin({ 22 | template: 'src/index.html' 23 | }), 24 | new webpack.EnvironmentPlugin({ 25 | NODE_ENV: 'production' 26 | }), 27 | new BundleAnalyzerPlugin({ 28 | analyzerMode: 'static', 29 | reportFilename: path.join(__dirname, 'bundle-report.html'), 30 | openAnalyzer: false, 31 | generateStatsFile: true, 32 | statsFilename: path.join(__dirname, 'stats.json') 33 | }) 34 | ], 35 | resolve: { 36 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.wasm'] 37 | }, 38 | experiments: { 39 | asyncWebAssembly: true 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.tsx?$/, 45 | exclude: /node_modules/, 46 | use: ['babel-loader'] 47 | }, 48 | { 49 | test: /\.(jpe?g|png|gif|svg)$/i, 50 | use: 'file-loader' 51 | } 52 | ] 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /ninres/src/bfres.rs: -------------------------------------------------------------------------------- 1 | //! Reads BFRES files. 2 | //! 3 | //! See http://mk8.tockdom.com/wiki/BFRES_(File_Format) 4 | 5 | use crate::{ByteOrderMark, Error, BNTX}; 6 | 7 | use std::io::SeekFrom; 8 | #[cfg(target_arch = "wasm32")] 9 | use wasm_bindgen::prelude::*; 10 | 11 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 12 | #[derive(Clone, Debug)] 13 | pub struct Bfres { 14 | version_number: u32, 15 | bom: ByteOrderMark, 16 | byte_alignment: u8, 17 | file_name_offset: u32, 18 | flags: u16, 19 | block_offset: u16, 20 | relocation_table_offset: u32, 21 | bfres_size: u32, 22 | file_name_length_offset: u64, 23 | embedded_files_offset: u64, 24 | embedded_files_dictionary_offset: u64, 25 | embedded_files_data_offset: u64, 26 | embedded_files_data_size: u64, 27 | embedded_files_count: u32, 28 | string_table_offset: u64, 29 | string_table_size: u32, 30 | embedded_files: Vec, 31 | } 32 | 33 | impl Bfres { 34 | pub fn new(buffer: &[u8]) -> Result { 35 | let mut bom = ByteOrderMark::try_new( 36 | buffer.to_vec(), 37 | u16::from_be_bytes([buffer[0xC], buffer[0xD]]), 38 | )?; 39 | bom.set_position(8); 40 | let version_number = bom.read_u32()?; 41 | let byte_alignment = buffer[0xE]; 42 | bom.seek(SeekFrom::Current(4))?; 43 | let file_name_offset = bom.read_u32()?; 44 | let flags = bom.read_u16()?; 45 | let block_offset = bom.read_u16()?; 46 | let relocation_table_offset = bom.read_u32()?; 47 | let bfres_size = bom.read_u32()?; 48 | let file_name_length_offset = bom.read_u64()?; 49 | 50 | bom.set_position(0xB8); 51 | let embedded_files_offset = bom.read_u64()?; 52 | let embedded_files_dictionary_offset = bom.read_u64()?; 53 | 54 | bom.seek(SeekFrom::Current(8))?; 55 | let string_table_offset = bom.read_u64()?; 56 | let string_table_size = bom.read_u32()?; 57 | 58 | bom.set_position(embedded_files_offset); 59 | let embedded_files_data_offset = bom.read_u64()?; 60 | let embedded_files_data_size = bom.read_u64()?; 61 | bom.set_position(embedded_files_dictionary_offset + 4); 62 | let embedded_files_count = bom.read_u32()?; 63 | 64 | let mut embedded_files = vec![]; 65 | for n in 0..embedded_files_count { 66 | let offset = if let Some(offset) = u64::checked_add( 67 | embedded_files_data_offset, 68 | if let Some(x) = u64::checked_mul(n as u64, embedded_files_data_size) { 69 | x 70 | } else { 71 | continue; 72 | }, 73 | ) { 74 | offset 75 | } else { 76 | continue; 77 | }; 78 | let end_offset = 79 | if let Some(end_offset) = u64::checked_add(offset, embedded_files_data_size) { 80 | end_offset as usize 81 | } else { 82 | continue; 83 | }; 84 | if buffer.len() < end_offset { 85 | continue; 86 | } 87 | let data = &buffer[offset as usize..end_offset]; 88 | 89 | let file = match std::str::from_utf8(&data[..4])? { 90 | "BNTX" => EmbeddedFile::BNTX(BNTX::try_new(data)?), 91 | _ => continue, 92 | }; 93 | 94 | embedded_files.push(file) 95 | } 96 | 97 | Ok(Bfres { 98 | version_number, 99 | bom, 100 | byte_alignment, 101 | file_name_offset, 102 | flags, 103 | block_offset, 104 | relocation_table_offset, 105 | bfres_size, 106 | file_name_length_offset, 107 | embedded_files_offset, 108 | embedded_files_dictionary_offset, 109 | embedded_files_data_offset, 110 | embedded_files_data_size, 111 | embedded_files_count, 112 | string_table_offset, 113 | string_table_size, 114 | embedded_files, 115 | }) 116 | } 117 | 118 | #[cfg(not(target_arch = "wasm32"))] 119 | pub fn get_embedded_files(&self) -> &Vec { 120 | &self.embedded_files 121 | } 122 | } 123 | 124 | #[cfg(target_arch = "wasm32")] 125 | #[wasm_bindgen] 126 | impl Bfres { 127 | #[wasm_bindgen(js_name = fromBytes)] 128 | pub fn from_bytes(buf: &[u8]) -> Result { 129 | Ok(Bfres::new(buf)?) 130 | } 131 | 132 | #[wasm_bindgen(js_name = intoBntxFiles)] 133 | pub fn into_bntx_files(self) -> Box<[JsValue]> { 134 | self.embedded_files 135 | .into_iter() 136 | .map(|t| match t { 137 | EmbeddedFile::BNTX(bntx) => bntx.into(), 138 | }) 139 | .collect() 140 | } 141 | } 142 | 143 | #[derive(Clone, Debug)] 144 | pub enum EmbeddedFile { 145 | BNTX(BNTX), 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use super::*; 151 | use test_case::test_case; 152 | 153 | static M1_PLAYER_MARIOMDL: &[u8] = include_bytes!("../../assets/M1_Player_MarioMdl.bfres"); 154 | 155 | #[test_case(M1_PLAYER_MARIOMDL; "with M1 Player MarioMdl")] 156 | fn test_read_bfres(bfres_file: &[u8]) { 157 | let bfres_file = Bfres::new(bfres_file); 158 | // dbg!(bfres_file.as_ref().unwrap()); 159 | 160 | assert!(bfres_file.is_ok()); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /ninres/src/bntx.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | use crate::{ByteOrderMark, Error}; 4 | 5 | #[cfg(target_arch = "wasm32")] 6 | use js_sys::JsString; 7 | use std::{cmp, collections::HashMap, convert::TryFrom, io::SeekFrom}; 8 | use util::*; 9 | #[cfg(target_arch = "wasm32")] 10 | use wasm_bindgen::prelude::*; 11 | 12 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 13 | #[derive(Clone, Debug)] 14 | pub struct BNTX { 15 | header: BNTXHeader, 16 | texture_count: i32, 17 | texture_array_offset: i64, 18 | texture_data_offset: i64, 19 | texture_dict_offset: i64, 20 | string_table_entries: HashMap, 21 | textures: Vec, 22 | } 23 | 24 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 25 | #[derive(Clone, Debug)] 26 | pub struct BNTXHeader { 27 | alignment: u8, 28 | target_address_size: u8, 29 | file_name_offset: u32, 30 | flag: u16, 31 | block_offset: u16, 32 | relocation_table_offset: u32, 33 | file_size: u32, 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub struct StringTableEntry { 38 | size: u16, 39 | string: String, 40 | } 41 | 42 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 43 | #[derive(Clone, Derivative)] 44 | #[derivative(Debug)] 45 | pub struct Texture { 46 | flags: u8, 47 | dim: u8, 48 | tile_mode: u16, 49 | swizzle: u16, 50 | mip_count: u16, 51 | sample_count: u16, 52 | format: u32, 53 | access_flags: u32, 54 | pub width: u32, 55 | pub height: u32, 56 | depth: u32, 57 | array_length: u32, 58 | texture_layout: u32, 59 | texture_layout2: u32, 60 | image_size: u32, 61 | alignment: u32, 62 | channel_type: u32, 63 | surface_dim: u8, 64 | name: String, 65 | parent_offset: u64, 66 | ptr_offset: u64, 67 | user_data_offset: u64, 68 | tex_ptr: u64, 69 | tex_view: u64, 70 | desc_slot_data_offset: u64, 71 | user_dict_offset: u64, 72 | mip_offsets: Vec, 73 | #[derivative(Debug = "ignore")] 74 | texture_data: Vec>>, 75 | } 76 | 77 | impl BNTX { 78 | pub fn try_new(buffer: &[u8]) -> Result { 79 | let mut bom = ByteOrderMark::try_new( 80 | buffer.to_vec(), 81 | u16::from_be_bytes([buffer[0xC], buffer[0xD]]), 82 | )?; 83 | let alignment = buffer[0xE]; 84 | let target_address_size = buffer[0xF]; 85 | bom.set_position(0x10); 86 | let file_name_offset = bom.read_u32()?; 87 | let flag = bom.read_u16()?; 88 | let block_offset = bom.read_u16()?; 89 | let relocation_table_offset = bom.read_u32()?; 90 | let file_size = bom.read_u32()?; 91 | 92 | bom.seek(SeekFrom::Current(4))?; 93 | let texture_count = bom.read_i32()?; 94 | let texture_array_offset = bom.read_i64()?; 95 | let texture_data_offset = bom.read_i64()?; 96 | let texture_dict_offset = bom.read_i64()?; 97 | 98 | bom.set_position(block_offset as u64 + 0x18); 99 | let mut string_table_entries = HashMap::with_capacity(texture_count as usize); 100 | for _ in 0..texture_count { 101 | let offset = bom.position(); 102 | let size = bom.read_u16()?; 103 | let string = std::str::from_utf8( 104 | &buffer[bom.position() as usize..(bom.position() + size as u64) as usize], 105 | )? 106 | .to_string(); 107 | string_table_entries.insert(offset, StringTableEntry { size, string }); 108 | bom.seek(SeekFrom::Current(size as i64))?; 109 | if bom.position() % 2 == 1 { 110 | bom.seek(SeekFrom::Current(1))?; 111 | } else { 112 | bom.seek(SeekFrom::Current(2))?; 113 | } 114 | } 115 | 116 | let mut textures = Vec::with_capacity(texture_count as usize); 117 | for i in 0..texture_count { 118 | bom.set_position(texture_array_offset as u64 + i as u64 * 8); 119 | let pos = bom.read_i64()?; 120 | if &<[u8; 4]>::try_from(&buffer[pos as usize..pos as usize + 4]).unwrap() != b"BRTI" { 121 | return Err(Error::CorruptData); 122 | } 123 | bom.set_position(pos as u64 + 0x10); 124 | let flags = bom.read_u8()?; 125 | let dim = bom.read_u8()?; 126 | let tile_mode = bom.read_u16()?; 127 | let swizzle = bom.read_u16()?; 128 | let mip_count = bom.read_u16()?; 129 | let sample_count = bom.read_u16()?; 130 | bom.seek(SeekFrom::Current(2))?; 131 | let format = bom.read_u32()?; 132 | 133 | let access_flags = bom.read_u32()?; 134 | let width = bom.read_u32()?; 135 | let height = bom.read_u32()?; 136 | let depth = bom.read_u32()?; 137 | let array_length = bom.read_u32()?; 138 | let texture_layout = bom.read_u32()?; 139 | let texture_layout2 = bom.read_u32()?; 140 | bom.seek(SeekFrom::Current(20))?; 141 | let image_size = bom.read_u32()?; 142 | 143 | let alignment = bom.read_u32()?; 144 | let channel_type = bom.read_u32()?; 145 | let surface_dim = bom.read_u8()?; 146 | bom.seek(SeekFrom::Current(3))?; 147 | let name_offset = bom.read_u64()?; 148 | let name = string_table_entries 149 | .get(&name_offset) 150 | .map_or_else(|| Err(Error::CorruptData), Ok)? 151 | .string 152 | .clone(); 153 | 154 | let parent_offset = bom.read_u64()?; 155 | let ptr_offset = bom.read_u64()?; 156 | let user_data_offset = bom.read_u64()?; 157 | let tex_ptr = bom.read_u64()?; 158 | let tex_view = bom.read_u64()?; 159 | let desc_slot_data_offset = bom.read_u64()?; 160 | let user_dict_offset = bom.read_u64()?; 161 | 162 | let mut mip_offsets = Vec::with_capacity(mip_count as usize); 163 | bom.set_position(ptr_offset); 164 | let first_mip_offset = bom.read_u64()?; 165 | mip_offsets.push(0); 166 | for _ in 1..mip_count { 167 | mip_offsets.push(bom.read_u64()? - first_mip_offset); 168 | } 169 | 170 | let mut texture_data = Vec::with_capacity(array_length as usize); 171 | bom.set_position(first_mip_offset); 172 | 173 | let (blk_width, blk_height) = 174 | if let Some((w, h)) = BLK_DIMS.lock().unwrap().get(&(format >> 8)) { 175 | (*w, *h) 176 | } else { 177 | (1, 1) 178 | }; 179 | let bpp = *BPPS.lock().unwrap().get(&(format >> 8)).unwrap(); 180 | let target = true; // "NX " 181 | 182 | let block_height_log2 = texture_layout & 7; 183 | let lines_per_block_height = (1 << block_height_log2) * 8; 184 | let mut block_height_shift = 0; 185 | 186 | for _ in 0..array_length { 187 | let mut mips = Vec::with_capacity(mip_count as usize); 188 | for (mip_level, mip_offset) in mip_offsets.iter().enumerate() { 189 | let size = (image_size as u64 - mip_offset) / array_length as u64; 190 | bom.set_position(first_mip_offset + *mip_offset); 191 | let buffer = (buffer 192 | [bom.position() as usize..(bom.position() + size) as usize]) 193 | .to_vec(); 194 | 195 | let width = cmp::max(1, width >> mip_level); 196 | let height = cmp::max(1, height >> mip_level); 197 | 198 | let size = 199 | div_round_up(width, blk_width) * div_round_up(height, blk_height) * bpp; 200 | 201 | if pow2_round_up(div_round_up(height, blk_height)) < lines_per_block_height { 202 | block_height_shift += 1; 203 | } 204 | 205 | let buffer = deswizzle( 206 | width, 207 | height, 208 | blk_width, 209 | blk_height, 210 | target, 211 | bpp, 212 | tile_mode, 213 | (block_height_log2) 214 | .checked_sub(block_height_shift) 215 | .unwrap_or_default(), 216 | buffer, 217 | )?; 218 | mips.push(buffer[..size as usize].to_vec()); 219 | } 220 | texture_data.push(mips); 221 | } 222 | 223 | textures.push(Texture { 224 | flags, 225 | dim, 226 | tile_mode, 227 | swizzle, 228 | mip_count, 229 | sample_count, 230 | format, 231 | access_flags, 232 | width, 233 | height, 234 | depth, 235 | array_length, 236 | texture_layout, 237 | texture_layout2, 238 | image_size, 239 | alignment, 240 | channel_type, 241 | surface_dim, 242 | name, 243 | parent_offset, 244 | ptr_offset, 245 | user_data_offset, 246 | tex_ptr, 247 | tex_view, 248 | desc_slot_data_offset, 249 | user_dict_offset, 250 | mip_offsets, 251 | texture_data, 252 | }); 253 | } 254 | 255 | let header = BNTXHeader { 256 | alignment, 257 | target_address_size, 258 | file_name_offset, 259 | flag, 260 | block_offset, 261 | relocation_table_offset, 262 | file_size, 263 | }; 264 | Ok(Self { 265 | header, 266 | texture_count, 267 | texture_array_offset, 268 | texture_data_offset, 269 | texture_dict_offset, 270 | string_table_entries, 271 | textures, 272 | }) 273 | } 274 | 275 | #[cfg(not(target_arch = "wasm32"))] 276 | pub fn get_textures(&self) -> &Vec { 277 | &self.textures 278 | } 279 | } 280 | 281 | #[cfg(target_arch = "wasm32")] 282 | #[wasm_bindgen] 283 | impl BNTX { 284 | #[wasm_bindgen(js_name = getTextures)] 285 | pub fn get_textures(&self) -> Box<[JsValue]> { 286 | self.textures 287 | .clone() 288 | .into_iter() 289 | .map(|t| t.into()) 290 | .collect() 291 | } 292 | } 293 | 294 | #[cfg(not(target_arch = "wasm32"))] 295 | impl Texture { 296 | pub fn get_name(&self) -> &String { 297 | &self.name 298 | } 299 | 300 | pub fn get_texture_data(&self) -> &Vec>> { 301 | &self.texture_data 302 | } 303 | } 304 | 305 | #[cfg(target_arch = "wasm32")] 306 | #[wasm_bindgen] 307 | impl Texture { 308 | #[wasm_bindgen(js_name = getName)] 309 | pub fn get_name(&self) -> JsString { 310 | self.name.clone().into() 311 | } 312 | 313 | #[wasm_bindgen(js_name = getTextureData)] 314 | pub fn get_texture_data(&self, tex_count: usize, mip_level: usize) -> Option> { 315 | self.texture_data 316 | .get(tex_count) 317 | .map(|d| d.get(mip_level)) 318 | .flatten() 319 | .cloned() 320 | .map(|d| { 321 | web_sys::console::log_1(&format!("{}", d.len()).into()); 322 | d.into_boxed_slice() 323 | }) 324 | } 325 | 326 | #[wasm_bindgen(js_name = getTexCount)] 327 | pub fn get_tex_count(&self) -> usize { 328 | self.texture_data.len() 329 | } 330 | 331 | #[wasm_bindgen(js_name = getMipLevel)] 332 | pub fn get_mip_level(&self, tex_count: usize) -> Option { 333 | self.texture_data.get(tex_count).map(|d| d.len()) 334 | } 335 | 336 | #[cfg(feature = "png")] 337 | #[wasm_bindgen(js_name = asPng)] 338 | pub fn as_png(&self, tex_count: usize, mip_level: usize) -> Option> { 339 | use image::{DynamicImage, ImageBuffer, ImageOutputFormat}; 340 | 341 | let width = cmp::max(1, self.width >> mip_level); 342 | let height = cmp::max(1, self.height >> mip_level); 343 | if let Some(buf) = self 344 | .texture_data 345 | .get(tex_count) 346 | .map(|d| d.get(mip_level)) 347 | .flatten() 348 | { 349 | if let Some(buf) = ImageBuffer::from_raw(width, height, buf.clone()) { 350 | let image = DynamicImage::ImageRgba8(buf); 351 | let mut res = vec![]; 352 | if let Err(err) = image.write_to(&mut res, ImageOutputFormat::Png) { 353 | web_sys::console::error_1(&format!("asPng threw an error: {}", err).into()); 354 | return None; 355 | } 356 | Some(res.into_boxed_slice()) 357 | } else { 358 | None 359 | } 360 | } else { 361 | None 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /ninres/src/bntx/util.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::{collections::HashMap, sync::Mutex}; 3 | 4 | use crate::Error; 5 | 6 | pub static BLK_DIMS: Lazy>> = Lazy::new(|| { 7 | let mut map = HashMap::new(); 8 | map.insert(0x1a, (4, 4)); 9 | map.insert(0x1b, (4, 4)); 10 | map.insert(0x1c, (4, 4)); 11 | map.insert(0x1d, (4, 4)); 12 | map.insert(0x1e, (4, 4)); 13 | map.insert(0x1f, (4, 4)); 14 | map.insert(0x20, (4, 4)); 15 | map.insert(0x2d, (4, 4)); 16 | map.insert(0x2e, (5, 4)); 17 | map.insert(0x2f, (5, 5)); 18 | map.insert(0x30, (6, 5)); 19 | map.insert(0x31, (6, 6)); 20 | map.insert(0x32, (8, 5)); 21 | map.insert(0x33, (8, 6)); 22 | map.insert(0x34, (8, 8)); 23 | map.insert(0x35, (10, 5)); 24 | map.insert(0x36, (10, 6)); 25 | map.insert(0x37, (10, 8)); 26 | map.insert(0x38, (10, 10)); 27 | map.insert(0x39, (12, 10)); 28 | map.insert(0x3a, (12, 12)); 29 | Mutex::new(map) 30 | }); 31 | 32 | pub static BPPS: Lazy>> = Lazy::new(|| { 33 | let mut map = HashMap::new(); 34 | map.insert(0x1, 1); 35 | map.insert(0x2, 1); 36 | map.insert(0x3, 2); 37 | map.insert(0x4, 2); 38 | map.insert(0x5, 2); 39 | map.insert(0x6, 2); 40 | map.insert(0x7, 2); 41 | map.insert(0x8, 2); 42 | map.insert(0x9, 2); 43 | map.insert(0xb, 4); 44 | map.insert(0xc, 4); 45 | map.insert(0xe, 4); 46 | map.insert(0x1a, 8); 47 | map.insert(0x1b, 0x10); 48 | map.insert(0x1c, 0x10); 49 | map.insert(0x1d, 8); 50 | map.insert(0x1e, 0x10); 51 | map.insert(0x1f, 0x10); 52 | map.insert(0x20, 0x10); 53 | map.insert(0x2d, 0x10); 54 | map.insert(0x2e, 0x10); 55 | map.insert(0x2f, 0x10); 56 | map.insert(0x30, 0x10); 57 | map.insert(0x31, 0x10); 58 | map.insert(0x32, 0x10); 59 | map.insert(0x33, 0x10); 60 | map.insert(0x34, 0x10); 61 | map.insert(0x35, 0x10); 62 | map.insert(0x36, 0x10); 63 | map.insert(0x37, 0x10); 64 | map.insert(0x38, 0x10); 65 | map.insert(0x39, 0x10); 66 | map.insert(0x3a, 0x10); 67 | map.insert(0x3b, 2); 68 | Mutex::new(map) 69 | }); 70 | 71 | #[inline] 72 | pub fn round_up(x: u32, y: u32) -> u32 { 73 | ((x - 1) | (y - 1)) + 1 74 | } 75 | 76 | #[inline] 77 | pub fn div_round_up(n: u32, d: u32) -> u32 { 78 | (n + d - 1) / d 79 | } 80 | 81 | #[inline] 82 | pub fn pow2_round_up(mut x: u32) -> u32 { 83 | x -= 1; 84 | x |= x >> 1; 85 | x |= x >> 2; 86 | x |= x >> 4; 87 | x |= x >> 8; 88 | x |= x >> 16; 89 | x + 1 90 | } 91 | 92 | pub fn get_addr_block_linear( 93 | mut x: u32, 94 | y: u32, 95 | width: u32, 96 | bpp: u32, 97 | base_addr: u32, 98 | block_height: u32, 99 | ) -> u32 { 100 | let image_width_in_gobs = div_round_up(width * bpp, 64); 101 | let gob_address = base_addr 102 | + (y / (8 * block_height)) * 512 * block_height * image_width_in_gobs 103 | + (x * bpp / 64) * 512 * block_height 104 | + (y % (8 * block_height) / 8) * 512; 105 | 106 | x *= bpp; 107 | 108 | gob_address 109 | + ((x % 64) / 32) * 256 110 | + ((y % 8) / 2) * 64 111 | + ((x % 32) / 16) * 32 112 | + (y % 2) * 16 113 | + (x % 16) 114 | } 115 | 116 | #[allow(unused)] 117 | pub fn get_block_height(height: u32) -> u32 { 118 | let mut block_height = pow2_round_up(height / 8); 119 | if block_height > 16 { 120 | block_height = 16; 121 | } 122 | block_height 123 | } 124 | 125 | #[allow(clippy::too_many_arguments)] 126 | pub fn deswizzle( 127 | width: u32, 128 | height: u32, 129 | blk_width: u32, 130 | blk_height: u32, 131 | round_pitch: bool, 132 | bpp: u32, 133 | tile_mode: u16, 134 | block_height_log2: u32, 135 | buffer: Vec, 136 | ) -> Result, Error> { 137 | if block_height_log2 > 5 { 138 | return Err(Error::CorruptData); 139 | } 140 | 141 | let block_height = 1 << block_height_log2; 142 | let width = div_round_up(width, blk_width); 143 | let height = div_round_up(height, blk_height); 144 | 145 | let (pitch, surf_size) = if tile_mode == 1 { 146 | let mut pitch = width * bpp; 147 | if round_pitch { 148 | pitch = round_up(pitch, 32) 149 | } 150 | let surf_size = pitch * height; 151 | (pitch, surf_size) 152 | } else { 153 | let pitch = round_up(width * bpp, 64); 154 | let surf_size = pitch * round_up(height, block_height * 8); 155 | (pitch, surf_size) 156 | }; 157 | 158 | let mut res = vec![0; surf_size as usize]; 159 | 160 | for y in 0..height { 161 | for x in 0..width { 162 | let pos = if tile_mode == 1 { 163 | y * pitch + x * bpp 164 | } else { 165 | get_addr_block_linear(x, y, width, bpp, 0, block_height) 166 | }; 167 | 168 | let pos2 = (y * width + x) * bpp; 169 | 170 | if pos + bpp <= surf_size { 171 | res[pos2 as usize..(pos2 + bpp) as usize] 172 | .copy_from_slice(&buffer[pos as usize..(pos + bpp) as usize]); 173 | } 174 | } 175 | } 176 | Ok(res) 177 | } 178 | -------------------------------------------------------------------------------- /ninres/src/bom.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, NinResError}; 2 | 3 | use byteorder::{ByteOrder, BE, LE}; 4 | use std::{ 5 | fmt::Debug, 6 | io::{Cursor, Seek, SeekFrom}, 7 | }; 8 | 9 | #[derive(Clone)] 10 | #[repr(u16)] 11 | pub enum ByteOrderMark { 12 | BigEndian(Cursor>), 13 | LittleEndian(Cursor>), 14 | } 15 | 16 | impl Debug for ByteOrderMark { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | match self { 19 | Self::BigEndian(_) => f.write_str("ByteOrderMark::BigEndian"), 20 | Self::LittleEndian(_) => f.write_str("ByteOrderMark::LittleEndian"), 21 | } 22 | } 23 | } 24 | 25 | #[cfg(any(feature = "bfres", feature = "sarc"))] 26 | impl ByteOrderMark { 27 | pub fn try_new(buffer: Vec, bom: u16) -> Result { 28 | match bom { 29 | 0xfeff => Ok(Self::BigEndian(Cursor::new(buffer))), 30 | 0xfffe => Ok(Self::LittleEndian(Cursor::new(buffer))), 31 | _ => Err(NinResError::ByteOrderInvalid), 32 | } 33 | } 34 | } 35 | 36 | macro_rules! read_number { 37 | ( $func:ident, $num:ty, 1 ) => { 38 | pub fn $func(&mut self) -> Result<$num, Error> { 39 | match self { 40 | Self::BigEndian(cursor) | Self::LittleEndian(cursor) => { 41 | let res = cursor.get_ref()[cursor.position() as usize]; 42 | cursor.seek(SeekFrom::Current(1))?; 43 | Ok(res) 44 | } 45 | } 46 | } 47 | }; 48 | ( $func:ident, $num:ty, $bytes:expr ) => { 49 | pub fn $func(&mut self) -> Result<$num, Error> { 50 | match self { 51 | Self::BigEndian(cursor) => { 52 | let res = BE::$func( 53 | &cursor.get_ref() 54 | [cursor.position() as usize..(cursor.position() + $bytes) as usize], 55 | ); 56 | cursor.seek(SeekFrom::Current($bytes))?; 57 | Ok(res) 58 | } 59 | Self::LittleEndian(cursor) => { 60 | let res = LE::$func( 61 | &cursor.get_ref() 62 | [cursor.position() as usize..(cursor.position() + $bytes) as usize], 63 | ); 64 | cursor.seek(SeekFrom::Current($bytes))?; 65 | Ok(res) 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | impl ByteOrderMark { 73 | pub fn position(&self) -> u64 { 74 | match self { 75 | Self::BigEndian(bytes) | Self::LittleEndian(bytes) => bytes.position(), 76 | } 77 | } 78 | 79 | pub fn set_position(&mut self, pos: u64) { 80 | match self { 81 | Self::BigEndian(bytes) | Self::LittleEndian(bytes) => bytes.set_position(pos), 82 | } 83 | } 84 | 85 | pub fn seek(&mut self, seek_from: SeekFrom) -> Result { 86 | match self { 87 | ByteOrderMark::BigEndian(cursor) | ByteOrderMark::LittleEndian(cursor) => { 88 | Ok(cursor.seek(seek_from)?) 89 | } 90 | } 91 | } 92 | 93 | read_number!(read_u8, u8, 1); 94 | read_number!(read_u16, u16, 2); 95 | read_number!(read_u32, u32, 4); 96 | read_number!(read_u64, u64, 8); 97 | read_number!(read_i16, i16, 2); 98 | read_number!(read_i32, i32, 4); 99 | read_number!(read_i64, i64, 8); 100 | } 101 | -------------------------------------------------------------------------------- /ninres/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{array::TryFromSliceError, str::Utf8Error, string::FromUtf8Error}; 2 | use thiserror::Error; 3 | #[cfg(target_arch = "wasm32")] 4 | use wasm_bindgen::prelude::*; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum NinResError { 8 | #[error("Type unknown or not implemented. Magic number: {0:?}")] 9 | TypeUnknownOrNotImplemented([u8; 4]), 10 | #[error(transparent)] 11 | IoError(#[from] std::io::Error), 12 | #[error("Byte order invalid")] 13 | ByteOrderInvalid, 14 | #[error("CorruptData")] 15 | CorruptData, 16 | #[error(transparent)] 17 | TryFromSlice(#[from] TryFromSliceError), 18 | #[error(transparent)] 19 | Utf8(#[from] Utf8Error), 20 | #[cfg(feature = "tar")] 21 | #[error("Tar append error")] 22 | TarAppend, 23 | #[cfg(feature = "zstd")] 24 | #[error("ZSTD error: {0}")] 25 | ZstdError(String), 26 | } 27 | 28 | impl<'a> From for NinResError { 29 | fn from(err: FromUtf8Error) -> Self { 30 | Self::Utf8(err.utf8_error()) 31 | } 32 | } 33 | 34 | #[cfg(target_arch = "wasm32")] 35 | impl From for JsValue { 36 | fn from(err: NinResError) -> JsValue { 37 | JsValue::from(format!("{}", err)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ninres/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Read commonly used Nintendo file formats. 2 | //! 3 | //! Please refer to the Wiki: 4 | //! https://github.com/Kinnay/Nintendo-File-Formats/wiki 5 | //! 6 | //! All file formats are behind feature flags. 7 | //! Here is a list of available Nintendo file format features: 8 | //! 9 | //! `bfres`, `sarc` 10 | //! 11 | //! You can also enable additional features: 12 | //! 13 | //! `tar`: write Nintendo resource to tar ball. 14 | //! 15 | //! `zstd`: ZSTD decompression. 16 | //! 17 | //! All features of this crate can be compiled to WebAssembly. 18 | //! 19 | //! # Examples 20 | //! 21 | //! Enable desired features in `Cargo.toml`. 22 | //! 23 | //! ```toml 24 | //! [dependencies] 25 | //! ninres = { version = "*", features = ["bfres", "sarc", "zstd"] } 26 | //! ``` 27 | //! 28 | //! In your `main.rs`. 29 | //! 30 | //! ``` 31 | //! # #[cfg(all(feature = "sarc", feature = "bfres"))] 32 | //! # use ninres::NinResResult; 33 | //! # #[cfg(all(feature = "sarc", feature = "bfres"))] 34 | //! # fn example() -> NinResResult { 35 | //! use std::fs::read; 36 | //! use ninres::{NinRes, NinResFile}; 37 | //! 38 | //! let buffer = read("foo.pack")?; 39 | //! let ninres = buffer.as_ninres()?; 40 | //! 41 | //! match &ninres { 42 | //! NinResFile::Bfres(_bfres) => {} 43 | //! NinResFile::Sarc(_sarc) => {} 44 | //! } 45 | //! 46 | //! Ok(ninres) 47 | //! # } 48 | //! ``` 49 | //! 50 | 51 | #[cfg(feature = "tar")] 52 | extern crate tar_crate as tar; 53 | 54 | #[macro_use] 55 | extern crate cfg_if; 56 | 57 | #[cfg(feature = "bfres")] 58 | #[macro_use] 59 | extern crate derivative; 60 | 61 | #[cfg(any(feature = "bfres", feature = "sarc"))] 62 | mod bom; 63 | mod error; 64 | 65 | #[cfg(feature = "bfres")] 66 | pub mod bfres; 67 | 68 | #[cfg(feature = "bfres")] 69 | pub mod bntx; 70 | 71 | #[cfg(feature = "sarc")] 72 | pub mod sarc; 73 | 74 | #[cfg(feature = "bfres")] 75 | pub use bfres::*; 76 | #[cfg(feature = "bfres")] 77 | pub use bntx::*; 78 | #[cfg(any(feature = "bfres", feature = "sarc"))] 79 | pub use bom::ByteOrderMark; 80 | pub use error::NinResError; 81 | #[cfg(feature = "sarc")] 82 | pub use sarc::*; 83 | 84 | #[cfg(target_arch = "wasm32")] 85 | use wasm_bindgen::prelude::*; 86 | 87 | #[cfg(any(feature = "bfres", feature = "sarc", feature = "tar"))] 88 | pub(crate) type Error = NinResError; 89 | #[cfg(any(feature = "bfres", feature = "sarc"))] 90 | pub type NinResResult = Result; 91 | 92 | #[cfg(any(feature = "bfres", feature = "sarc"))] 93 | #[cfg(not(target_arch = "wasm32"))] 94 | #[derive(Clone, Debug)] 95 | pub enum NinResFile { 96 | #[cfg(feature = "bfres")] 97 | Bfres(bfres::Bfres), 98 | #[cfg(feature = "sarc")] 99 | Sarc(sarc::Sarc), 100 | } 101 | 102 | #[cfg(any(feature = "bfres", feature = "sarc"))] 103 | #[cfg(target_arch = "wasm32")] 104 | #[wasm_bindgen] 105 | #[derive(Clone, Debug)] 106 | pub enum NinResFile { 107 | #[cfg(feature = "bfres")] 108 | Bfres, 109 | #[cfg(feature = "sarc")] 110 | Sarc, 111 | } 112 | 113 | #[cfg(any(feature = "bfres", feature = "sarc"))] 114 | #[cfg(not(target_arch = "wasm32"))] 115 | impl NinResFile { 116 | pub fn get_extension(&self) -> &str { 117 | match self { 118 | #[cfg(feature = "bfres")] 119 | Self::Bfres(_) => "bfres", 120 | #[cfg(feature = "sarc")] 121 | Self::Sarc(_) => "sarc", 122 | } 123 | } 124 | } 125 | 126 | #[cfg(target_arch = "wasm32")] 127 | #[wasm_bindgen] 128 | #[derive(Clone, Debug)] 129 | pub struct NinResFileExt { 130 | file_type: NinResFile, 131 | sarc: Option, 132 | bfres: Option, 133 | } 134 | 135 | #[cfg(target_arch = "wasm32")] 136 | #[wasm_bindgen] 137 | impl NinResFileExt { 138 | #[wasm_bindgen(js_name = getFileType)] 139 | pub fn get_file_type(&self) -> NinResFile { 140 | self.file_type.clone() 141 | } 142 | 143 | #[wasm_bindgen(js_name = getSarc)] 144 | pub fn get_sarc(&self) -> Option { 145 | self.sarc.clone() 146 | } 147 | 148 | #[wasm_bindgen(js_name = getBfres)] 149 | pub fn get_bfres(&self) -> Option { 150 | self.bfres.clone() 151 | } 152 | } 153 | 154 | /// Smart convert buffer into any known Nintendo file format. 155 | /// 156 | /// # Examples 157 | /// 158 | /// ``` 159 | /// # use ninres::NinResResult; 160 | /// # #[cfg(all(feature = "sarc", feature = "bfres"))] 161 | /// # fn example() -> NinResResult { 162 | /// use std::fs::read; 163 | /// use ninres::{NinRes, NinResFile}; 164 | /// 165 | /// let buffer = read("foo.pack")?; 166 | /// let ninres = buffer.as_ninres()?; 167 | /// 168 | /// match &ninres { 169 | /// NinResFile::Bfres(_bfres) => {} 170 | /// NinResFile::Sarc(_sarc) => {} 171 | /// } 172 | /// 173 | /// Ok(ninres) 174 | /// # } 175 | /// ``` 176 | #[cfg(any(feature = "bfres", feature = "sarc"))] 177 | #[cfg(not(target_arch = "wasm32"))] 178 | pub trait NinRes { 179 | fn as_ninres(&self) -> NinResResult; 180 | fn into_ninres(self) -> NinResResult; 181 | } 182 | 183 | #[cfg(any(feature = "bfres", feature = "sarc"))] 184 | #[cfg(not(target_arch = "wasm32"))] 185 | impl NinRes for &[u8] { 186 | fn as_ninres(&self) -> NinResResult { 187 | let decompressed = if b"\x28\xB5\x2F\xFD" == &self[..4] { 188 | use std::io::{Cursor, Read}; 189 | let mut decompressed = vec![]; 190 | let mut cursor = Cursor::new(self); 191 | let mut decoder = 192 | ruzstd::StreamingDecoder::new(&mut cursor).map_err(Error::ZstdError)?; 193 | 194 | decoder.read_to_end(&mut decompressed).unwrap(); 195 | Some(decompressed) 196 | } else { 197 | None 198 | }; 199 | let data = if let Some(decompressed) = &decompressed { 200 | &decompressed[..] 201 | } else { 202 | self 203 | }; 204 | 205 | match std::str::from_utf8(&data[..4])? { 206 | #[cfg(feature = "sarc")] 207 | "SARC" => Ok(NinResFile::Sarc(Sarc::new(data)?)), 208 | #[cfg(feature = "bfres")] 209 | "FRES" => Ok(NinResFile::Bfres(Bfres::new(data)?)), 210 | _ => Err(NinResError::TypeUnknownOrNotImplemented([ 211 | data[0], data[1], data[2], data[3], 212 | ])), 213 | } 214 | } 215 | 216 | fn into_ninres(self) -> NinResResult { 217 | let decompressed = if b"\x28\xB5\x2F\xFD" == &self[..4] { 218 | use std::io::{Cursor, Read}; 219 | let mut decompressed = vec![]; 220 | let mut cursor = Cursor::new(self); 221 | let mut decoder = 222 | ruzstd::StreamingDecoder::new(&mut cursor).map_err(Error::ZstdError)?; 223 | 224 | decoder.read_to_end(&mut decompressed).unwrap(); 225 | Some(decompressed) 226 | } else { 227 | None 228 | }; 229 | let data = if let Some(decompressed) = &decompressed { 230 | &decompressed[..] 231 | } else { 232 | self 233 | }; 234 | 235 | match std::str::from_utf8(&data[..4])? { 236 | #[cfg(feature = "sarc")] 237 | "SARC" => Ok(NinResFile::Sarc(Sarc::new(data)?)), 238 | #[cfg(feature = "bfres")] 239 | "FRES" => Ok(NinResFile::Bfres(Bfres::new(data)?)), 240 | _ => Err(NinResError::TypeUnknownOrNotImplemented([ 241 | data[0], data[1], data[2], data[3], 242 | ])), 243 | } 244 | } 245 | } 246 | 247 | #[cfg(any(feature = "bfres", feature = "sarc"))] 248 | #[cfg(not(target_arch = "wasm32"))] 249 | impl NinRes for Vec { 250 | fn as_ninres(&self) -> NinResResult { 251 | let decompressed = if b"\x28\xB5\x2F\xFD" == &self[..4] { 252 | use std::io::{Cursor, Read}; 253 | let mut decompressed = vec![]; 254 | let mut cursor = Cursor::new(self); 255 | let mut decoder = 256 | ruzstd::StreamingDecoder::new(&mut cursor).map_err(Error::ZstdError)?; 257 | 258 | decoder.read_to_end(&mut decompressed).unwrap(); 259 | Some(decompressed) 260 | } else { 261 | None 262 | }; 263 | let data = if let Some(decompressed) = &decompressed { 264 | decompressed 265 | } else { 266 | self 267 | }; 268 | 269 | match std::str::from_utf8(&data[..4])? { 270 | #[cfg(feature = "sarc")] 271 | "SARC" => Ok(NinResFile::Sarc(Sarc::new(data)?)), 272 | #[cfg(feature = "bfres")] 273 | "FRES" => Ok(NinResFile::Bfres(Bfres::new(data)?)), 274 | _ => Err(NinResError::TypeUnknownOrNotImplemented([ 275 | data[0], data[1], data[2], data[3], 276 | ])), 277 | } 278 | } 279 | 280 | fn into_ninres(mut self) -> NinResResult { 281 | if b"\x28\xB5\x2F\xFD" == &self[..4] { 282 | use std::io::{Cursor, Read}; 283 | let mut decompressed = vec![]; 284 | let mut cursor = Cursor::new(self); 285 | let mut decoder = 286 | ruzstd::StreamingDecoder::new(&mut cursor).map_err(Error::ZstdError)?; 287 | 288 | decoder.read_to_end(&mut decompressed).unwrap(); 289 | self = decompressed; 290 | } 291 | 292 | match std::str::from_utf8(&self[..4])? { 293 | #[cfg(feature = "sarc")] 294 | "SARC" => Ok(NinResFile::Sarc(Sarc::new(&self[..])?)), 295 | #[cfg(feature = "bfres")] 296 | "FRES" => Ok(NinResFile::Bfres(Bfres::new(&self[..])?)), 297 | _ => Err(NinResError::TypeUnknownOrNotImplemented([ 298 | self[0], self[1], self[2], self[3], 299 | ])), 300 | } 301 | } 302 | } 303 | 304 | #[cfg(any(feature = "bfres", feature = "sarc"))] 305 | #[cfg(target_arch = "wasm32")] 306 | #[wasm_bindgen] 307 | impl NinResFileExt { 308 | #[wasm_bindgen(js_name = fromBytes)] 309 | pub fn from_bytes(buf: &[u8]) -> Result { 310 | match std::str::from_utf8(&buf[..4]).map_err(|err| JsValue::from(format!("{}", err)))? { 311 | #[cfg(feature = "sarc")] 312 | "SARC" => Ok(NinResFileExt { 313 | file_type: NinResFile::Sarc, 314 | sarc: Some(Sarc::new(buf)?), 315 | bfres: None, 316 | }), 317 | #[cfg(feature = "bfres")] 318 | "FRES" => Ok(NinResFileExt { 319 | file_type: NinResFile::Bfres, 320 | sarc: None, 321 | bfres: Some(Bfres::new(buf)?), 322 | }), 323 | _ => Err( 324 | NinResError::TypeUnknownOrNotImplemented([buf[0], buf[1], buf[2], buf[3]]).into(), 325 | ), 326 | } 327 | } 328 | } 329 | 330 | /// Convert resource into tar buffer. 331 | /// This buffer can then e.g. be stored in a file. 332 | /// 333 | /// The `mode` parameter refers to the file mode within the tar ball. 334 | /// 335 | /// # Examples 336 | /// 337 | /// ``` 338 | /// # use ninres::NinResError; 339 | /// #[cfg(all(not(target_arch = "wasm32"), feature = "sarc"))] 340 | /// fn main() -> Result<(), NinResError> { 341 | /// use ninres::{sarc::Sarc, IntoTar}; 342 | /// use std::{fs::{read, File}, io::Write}; 343 | /// 344 | /// let sarc_file = Sarc::new(&read("../assets/M1_Model.pack")?)?; 345 | /// let tar = sarc_file.into_tar(0o644)?; 346 | /// 347 | /// let mut file = File::create("M1_Model.tar")?; 348 | /// file.write_all(&tar.into_inner()[..])?; 349 | /// Ok(()) 350 | /// } 351 | /// ``` 352 | #[cfg(feature = "tar")] 353 | pub trait IntoTar { 354 | fn into_tar(self, mode: u32) -> Result>, Error>; 355 | } 356 | 357 | cfg_if! { 358 | if #[cfg(target_arch = "wasm32")] { 359 | /// Setup panic hook for WebAssembly calls. 360 | /// This will forward Rust panics to console.error 361 | #[wasm_bindgen(js_name = setupPanicHook)] 362 | pub fn setup_panic_hook() { 363 | console_error_panic_hook::set_once(); 364 | } 365 | 366 | #[global_allocator] 367 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /ninres/src/sarc.rs: -------------------------------------------------------------------------------- 1 | //! Reads SARC files. 2 | //! 3 | //! See http://mk8.tockdom.com/wiki/SARC_(File_Format) 4 | 5 | #[cfg(feature = "tar")] 6 | use crate::IntoTar; 7 | use crate::{ByteOrderMark, Error}; 8 | 9 | #[cfg(target_arch = "wasm32")] 10 | use js_sys::JsString; 11 | #[cfg(any(feature = "tar", feature = "zstd"))] 12 | use std::io::Cursor; 13 | use std::io::SeekFrom; 14 | #[cfg(target_arch = "wasm32")] 15 | use wasm_bindgen::prelude::*; 16 | 17 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 18 | #[derive(Clone, Debug)] 19 | pub struct Sarc { 20 | header: SarcHeader, 21 | sfat_header: SfatHeader, 22 | sfat_nodes: Vec, 23 | } 24 | 25 | #[derive(Clone, Debug)] 26 | pub struct SarcHeader { 27 | pub byte_order: ByteOrderMark, 28 | pub file_size: u32, 29 | pub data_offset: u32, 30 | pub version_number: u16, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct SfatHeader { 35 | pub node_count: u16, 36 | } 37 | 38 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 39 | #[derive(Clone, Debug)] 40 | pub struct SfatNode { 41 | pub hash: u32, 42 | pub attributes: u32, 43 | pub path_table_offset: Option, 44 | path: Option, 45 | pub data_start_offset: u32, 46 | pub data_end_offset: u32, 47 | data: Vec, 48 | #[cfg(feature = "zstd")] 49 | data_decompressed: Option>, 50 | } 51 | 52 | impl Sarc { 53 | #[cfg(not(target_arch = "wasm32"))] 54 | pub fn get_sfat_nodes(&self) -> &Vec { 55 | &self.sfat_nodes 56 | } 57 | 58 | pub fn new(buffer: &[u8]) -> Result { 59 | let mut bom = 60 | ByteOrderMark::try_new(buffer.to_vec(), u16::from_be_bytes([buffer[6], buffer[7]]))?; 61 | bom.set_position(8); 62 | let file_size = bom.read_u32()?; 63 | let data_offset = bom.read_u32()?; 64 | let version_number = bom.read_u16()?; 65 | bom.seek(SeekFrom::Current(8))?; 66 | let node_count = bom.read_u16()?; 67 | let mut sfat_nodes = vec![]; 68 | let file_name_table_offset = (0x14 + 0xC + node_count as u32 * 0x10) as usize; 69 | for i in 0..node_count { 70 | let offset = (0x14 + 0xC + i * 0x10) as u64; 71 | bom.set_position(offset); 72 | let hash = bom.read_u32()?; 73 | let attributes = bom.read_u32()?; 74 | let name_table_offset = if attributes & 0xffff0000 == 0x01000000 { 75 | Some((attributes & 0x0000ffff) * 4) 76 | } else { 77 | None 78 | }; 79 | let name = if let Some(name_table_offset) = name_table_offset { 80 | let name = buffer[(file_name_table_offset + name_table_offset as usize + 8)..] 81 | .iter() 82 | .take_while(|&n| n != &0u8) 83 | .cloned() 84 | .collect::<_>(); 85 | Some(String::from_utf8(name)?) 86 | } else { 87 | None 88 | }; 89 | let data_start_offset = bom.read_u32()?; 90 | let data_end_offset = bom.read_u32()?; 91 | let data = &buffer[(data_offset + data_start_offset) as usize 92 | ..(data_offset + data_end_offset) as usize]; 93 | sfat_nodes.push(SfatNode { 94 | hash, 95 | attributes, 96 | path_table_offset: name_table_offset, 97 | path: name, 98 | data_start_offset, 99 | data_end_offset, 100 | data: data.to_vec(), 101 | #[cfg(feature = "zstd")] 102 | data_decompressed: if b"\x28\xB5\x2F\xFD" == &data[..4] { 103 | use std::io::Read; 104 | let mut decompressed = vec![]; 105 | let mut cursor = Cursor::new(data); 106 | let mut decoder = 107 | ruzstd::StreamingDecoder::new(&mut cursor).map_err(Error::ZstdError)?; 108 | 109 | decoder.read_to_end(&mut decompressed).unwrap(); 110 | Some(decompressed) 111 | } else { 112 | None 113 | }, 114 | }) 115 | } 116 | Ok(Sarc { 117 | header: SarcHeader { 118 | byte_order: bom, 119 | file_size, 120 | data_offset, 121 | version_number, 122 | }, 123 | sfat_header: SfatHeader { node_count }, 124 | sfat_nodes, 125 | }) 126 | } 127 | } 128 | 129 | #[cfg(target_arch = "wasm32")] 130 | #[wasm_bindgen] 131 | impl Sarc { 132 | #[wasm_bindgen(js_name = getSfatNodes)] 133 | pub fn get_sfat_nodes(&self) -> Box<[JsValue]> { 134 | self.sfat_nodes.iter().cloned().map(|n| n.into()).collect() 135 | } 136 | 137 | #[wasm_bindgen(js_name = intoSfatNodes)] 138 | pub fn into_sfat_nodes(self) -> Box<[JsValue]> { 139 | self.sfat_nodes.into_iter().map(|n| n.into()).collect() 140 | } 141 | } 142 | 143 | #[cfg(feature = "tar")] 144 | impl IntoTar for Sarc { 145 | fn into_tar(self, mode: u32) -> Result>, Error> { 146 | use std::time::SystemTime; 147 | 148 | let res = vec![]; 149 | let cursor = Cursor::new(res); 150 | let mut builder = tar::Builder::new(cursor); 151 | let mtime = SystemTime::now() 152 | .duration_since(SystemTime::UNIX_EPOCH) 153 | .unwrap() 154 | .as_secs(); 155 | 156 | self.sfat_nodes 157 | .into_iter() 158 | .try_for_each(|node| -> Result<(), Error> { 159 | if let Some(name) = node.path { 160 | let mut header = tar::Header::new_gnu(); 161 | header.set_size(node.data.len() as u64); 162 | header.set_mode(mode); 163 | header.set_mtime(mtime); 164 | cfg_if! { 165 | if #[cfg(feature = "zstd")] { 166 | builder.append_data(&mut header, name.clone(), &node.data[..])?; 167 | if let Some(data_deflated) = node.data_decompressed { 168 | let mut header = tar::Header::new_gnu(); 169 | header.set_size(data_deflated.len() as u64); 170 | header.set_cksum(); 171 | builder.append_data(&mut header, format!("{}.tar", name), &data_deflated[..])?; 172 | } 173 | } else { 174 | header.set_cksum(); 175 | builder.append_data(&mut header, name, &node.data[..])?; 176 | } 177 | } 178 | } 179 | Ok(()) 180 | })?; 181 | builder.finish()?; 182 | Ok(builder.into_inner()?) 183 | } 184 | } 185 | 186 | #[cfg(not(target_arch = "wasm32"))] 187 | impl SfatNode { 188 | pub fn get_path(&self) -> Option<&String> { 189 | self.path.as_ref() 190 | } 191 | 192 | pub fn get_data(&self) -> &Vec { 193 | &self.data 194 | } 195 | 196 | pub fn get_data_decompressed(&self) -> Option<&Vec> { 197 | self.data_decompressed.as_ref() 198 | } 199 | 200 | fn _get_hash(data: &[u32], length: usize, key: u32) -> u32 { 201 | let mut result = 0; 202 | #[allow(clippy::needless_range_loop)] 203 | for i in 0..length { 204 | result = data[i] + result * key; 205 | } 206 | result 207 | } 208 | } 209 | 210 | #[cfg(target_arch = "wasm32")] 211 | #[wasm_bindgen] 212 | impl SfatNode { 213 | #[wasm_bindgen(js_name = getPath)] 214 | pub fn get_path(&self) -> Option { 215 | self.path.clone().map(|p| p.into()) 216 | } 217 | 218 | #[wasm_bindgen(js_name = intoData)] 219 | pub fn into_data(self) -> Box<[u8]> { 220 | if let Some(data) = self.data_decompressed { 221 | data.into_boxed_slice() 222 | } else { 223 | self.data.into_boxed_slice() 224 | } 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use super::*; 231 | use test_case::test_case; 232 | 233 | static M1_MODEL_PACK: &[u8] = include_bytes!("../../assets/M1_Model.pack"); 234 | static M3_MODEL_PACK: &[u8] = include_bytes!("../../assets/M3_Model.pack"); 235 | static MW_MODEL_PACK: &[u8] = include_bytes!("../../assets/MW_Model.pack"); 236 | 237 | #[test_case(M1_MODEL_PACK; "with M1 Model Pack")] 238 | #[test_case(M3_MODEL_PACK; "with M3 Model Pack")] 239 | #[test_case(MW_MODEL_PACK; "with MW Model Pack")] 240 | fn test_read_sarc(sarc_file: &[u8]) { 241 | let sarc_file = Sarc::new(sarc_file); 242 | 243 | assert!(sarc_file.is_ok()); 244 | } 245 | 246 | #[cfg(feature = "tar")] 247 | #[test_case(M1_MODEL_PACK, "M1_Model.tar"; "with M1 Model Pack")] 248 | #[test_case(M3_MODEL_PACK, "M3_Model.tar"; "with M3 Model Pack")] 249 | #[test_case(MW_MODEL_PACK, "MW_Model.tar"; "with MW Model Pack")] 250 | fn test_into_tar(sarc_file: &[u8], file_name: &str) -> Result<(), Error> { 251 | let sarc_file = Sarc::new(sarc_file)?; 252 | let tar = sarc_file.into_tar(0o644)?; 253 | 254 | use std::io::Write; 255 | let mut file = std::fs::File::create(file_name)?; 256 | file.write_all(&tar.into_inner()[..])?; 257 | Ok(()) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Mario Reder ", 3 | "name": "ninres", 4 | "version": "auto", 5 | "repository": "https://github.com/Tarnadas/ninres-rs", 6 | "license": "MIT", 7 | "scripts": { 8 | "build:web": "CARGO_PROFILE_RELEASE_OPT_LEVEL=z wasm-pack build --release --target=browser --out-name=ninres ninres -- --all-features", 9 | "build:web-dev": "wasm-pack build --dev --target=browser --out-name=ninres ninres -- --all-features", 10 | "watch": "cargo watch -w ninres/src -s 'wasm-pack build --dev --target=browser --out-name=ninres ninres -- --all-features'" 11 | } 12 | } 13 | --------------------------------------------------------------------------------