From adde636191258c497398008cdc88394c8422e9f7 Mon Sep 17 00:00:00 2001 From: Noxturnix Date: Sun, 9 Oct 2022 23:33:25 -0700 Subject: [PATCH] init --- .github/workflows/release.yml | 90 ++++++++++++ .gitignore | 2 + Cargo.lock | 264 ++++++++++++++++++++++++++++++++++ Cargo.toml | 16 +++ LICENSE | 7 + README.md | 37 +++++ src/cli.rs | 40 ++++++ src/file.rs | 20 +++ src/lib.rs | 102 +++++++++++++ src/main.rs | 77 ++++++++++ 10 files changed, 655 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/cli.rs create mode 100644 src/file.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9fbfbb9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +on: + push: + tags: + - "v*" + +env: + CARGO_TERM_COLOR: always + +jobs: + publish: + name: Binary ${{ matrix.target }} (on ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + outputs: + version: ${{ steps.extract_version.outputs.version }} + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + cross: true + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + cross: true + - os: ubuntu-latest + target: armv7-unknown-linux-musleabihf + cross: true + - os: ubuntu-latest + target: arm-unknown-linux-musleabihf + cross: true + - os: windows-latest + target: x86_64-pc-windows-gnu + cross: false + - os: macos-latest + target: x86_64-apple-darwin + cross: false + - os: ubuntu-latest + target: wasm32-wasi + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + + - run: sudo apt install musl-tools + if: startsWith(matrix.os, 'ubuntu') && matrix.target != 'wasm32-wasi' + + - name: cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --locked --target=${{ matrix.target }} + use-cross: ${{ matrix.cross }} + + - name: Set exe extension for Windows + run: echo "EXE=.exe" >> $env:GITHUB_ENV + if: startsWith(matrix.os, 'windows') + + - name: Set wasm extension for WASM + run: echo "WASM=.wasm" >> $GITHUB_ENV + if: matrix.target == 'wasm32-wasi' + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.target }} + path: target/${{ matrix.target }}/release/flipnote-id${{ env.EXE }}${{ env.WASM }} + + - name: Get version from tag + id: extract_version + run: | + echo ::set-output name=version::${GITHUB_REF_NAME#v} + shell: bash + + - name: Release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: target/${{ matrix.target }}/release/flipnote-id${{ env.EXE }}${{ env.WASM }} + tag: ${{ github.ref_name }} + asset_name: flipnote-id-${{ steps.extract_version.outputs.version }}-${{ matrix.target }}${{ env.EXE }}${{ env.WASM }} + body: v${{ steps.extract_version.outputs.version }} + if: startsWith(github.ref_name, 'v') && github.ref_type == 'tag' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ae0869 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +option.bin* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..25e3cc3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,264 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "clap" +version = "4.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1a0a4208c6c483b952ad35c6eed505fc13b46f08f631b81e828084a9318d74" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db342ce9fda24fb191e2ed4e102055a4d381c1086a06630174cd8da8d5d917ce" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote 1.0.21", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "flipnote-id" +version = "1.0.0" +dependencies = [ + "clap", + "hex", + "structure", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "libc" +version = "0.2.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote 1.0.21", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote 1.0.21", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f95648580798cc44ff8efb9bb0d7ee5205ea32e087b31b0732f3e8c2648ee2" +dependencies = [ + "proc-macro-hack-impl", +] + +[[package]] +name = "proc-macro-hack-impl" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027" + +[[package]] +name = "proc-macro2" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structure" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8379aeb9cf8935b018b14d9191b15096e984ad8b77424b9bce075ea15d1fa59" +dependencies = [ + "byteorder", + "proc-macro-hack", + "structure-macro-impl", +] + +[[package]] +name = "structure-macro-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c0529f2429c8bb5878688fffab7f700087f4bd47906e6acf03fd7361f77aca" +dependencies = [ + "proc-macro-hack", + "quote 0.3.15", +] + +[[package]] +name = "syn" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +dependencies = [ + "proc-macro2", + "quote 1.0.21", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c676bef --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "flipnote-id" +version = "1.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.0.10", features = ["derive"] } +hex = "0.4.3" +structure = "0.1.2" + +[profile.release] +lto = true +opt-level = 3 +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8c21ba --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 Sirawit Thaya (Noxturnix) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04b245b --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# flipnote-id + +A tool to modify Flipnote Studio ID (FSID) implemented in Rust + +``` +Usage: flipnote-id [OPTIONS] + +Arguments: + [possible values: set, extract, check] + +Options: + -f, --file Flipnote Studio option file [default: option.bin] + -i, --id Flipnote Studio ID (use with `set` action) + -d, --no-backup Don't backup the original file when setting FSID (used by `set` action) + -h, --help Print help information (use `--help` for more detail) + -V, --version Print version information +``` + +# Example + +``` +$ ./flipnote-id set --id 5000000000000000 --file option.bin +``` + +# Caution + +I'm not responsible for any kind of data loss or service terminations (including bans from custom Flipnote servers) + +# Credits + +- Thanks [Flipnote Collective](https://github.com/Flipnote-Collective) for providing [FSID format](https://github.com/Flipnote-Collective/flipnote-studio-docs/wiki/FSIDs-and-Filenames#flipnote-studio-ids) info +- Thanks nocash for providing [option.bin file strucure](https://problemkaputt.de/gbatek-dsi-sd-mmc-flipnote-files.htm) +- [genact](https://github.com/svenstaro/genact) project as an example for Rust GitHub Actions release file and Cargo.toml options + +# License + +[MIT License](LICENSE) diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..35c9875 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,40 @@ +use clap::{ArgAction, Parser, ValueEnum, ValueHint}; + +#[derive(Parser)] +#[command(version)] +#[command(propagate_version = true)] +#[command(arg_required_else_help = true)] +pub struct Cli { + #[arg(value_enum)] + pub action: Actions, + + /// Flipnote Studio option file + #[arg(default_value = "option.bin")] + #[arg(long = "file")] + #[arg(short = 'f')] + #[arg(value_hint = ValueHint::FilePath)] + pub file: String, + + /// Flipnote Studio ID (use with `set` action) + #[arg(long = "id")] + #[arg(short = 'i')] + #[arg(value_hint = ValueHint::FilePath)] + #[arg(required_if_eq("action", "set"))] + pub fsid: Option, + + /// Don't backup the original file when setting FSID (used by `set` action) + #[arg(action = ArgAction::SetTrue)] + #[arg(long = "no-backup")] + #[arg(short = 'd')] + pub no_backup: bool, +} + +#[derive(ValueEnum, Clone)] +pub enum Actions { + /// Set FSID and compute checksum + Set, + /// Extract FSID + Extract, + /// Check FSID and verify checksum + Check, +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..ead7251 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,20 @@ +use std::io::Error; +use std::io::ErrorKind; +use std::process::exit; + +use flipnote_id::FlipnoteDataError; + +pub fn handle_file_read_error(err: Error, file_path: &String) { + match err.kind() { + ErrorKind::NotFound => eprintln!("Error: File `{}` not found", file_path), + _ => eprintln!("Error: Can't read file"), + } + exit(1); +} + +pub fn handle_flipnote_id_error(err: FlipnoteDataError) { + match err { + FlipnoteDataError::InvalidSize => eprintln!("Error: Invalid file size"), + } + exit(1); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..28449c2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,102 @@ +#[macro_use] +extern crate structure; + +use hex::decode; + +#[derive(Debug)] +pub struct FlipnoteId { + pub id: u64, + pub checksum: u16, +} + +#[derive(Debug)] +pub enum FlipnoteDataError { + InvalidSize, +} + +fn check_data_length(data: &Vec) -> bool { + return data.len() == 256; +} + +pub fn extract_id_with_checksum(data: &Vec) -> Result { + if !check_data_length(data) { + return Err(FlipnoteDataError::InvalidSize); + } + + let flipnote_option_structure = structure!(") -> Result { + if !check_data_length(data) { + return Err(FlipnoteDataError::InvalidSize); + } + + let data_without_checksum = [&data[..24], &[0u8; 2], &data[26..]].concat(); + let mut checksum = 0u16; + + for (i, b) in data_without_checksum.iter().enumerate() { + checksum += (b ^ i as u8) as u16; + } + + return Ok(checksum); +} + +pub fn decode_fsid(fsid: &String) -> Result { + if fsid.len() != 16 { + return Err(()); + } + + let fsid_bytes = decode(fsid); + + if let Err(_) = fsid_bytes { + return Err(()); + } + + let fsid_bytes = fsid_bytes.unwrap(); + + match &fsid_bytes[0] >> 4 { + 0 | 1 | 5 | 9 => {} + _ => return Err(()), + } + + if &fsid_bytes[3] & 0b1111 != 0 { + return Err(()); + } + + let fsid_structure = structure!("Q"); + return Ok(fsid_structure.unpack(fsid_bytes).unwrap().0); +} + +pub fn set_fsid(data: &Vec, fsid: &String) -> Result, ()> { + let fsid_num = decode_fsid(fsid); + + if let Err(_) = fsid_num { + return Err(()); + } + + let fsid_num = fsid_num.unwrap(); + let fsid_structure = structure!(" match read(&cli.file) { + Ok(bin_data) => match set_fsid(&bin_data, &cli.fsid.unwrap()) { + Ok(new_data) => { + if !cli.no_backup { + if let Err(_) = copy(&cli.file, format!("{}.bak", &cli.file)) { + eprintln!("Error: Can't make a backup file"); + exit(1); + } + } + + let mut fp = File::create(&cli.file).unwrap(); + match fp.write_all(&new_data) { + Ok(_) => println!( + "Successfully set FSID to {:016X}", + extract_id_with_checksum(&new_data).unwrap().id + ), + Err(_) => { + eprintln!("Error: Can't write to file"); + exit(1); + } + } + } + Err(_) => { + eprintln!("Error: Invalid FSID"); + exit(1); + } + }, + Err(err) => handle_file_read_error(err, &cli.file), + }, + Actions::Extract => match read(&cli.file) { + Ok(bin_data) => match extract_id_with_checksum(&bin_data) { + Ok(flipnote_id) => println!("{:016X}", flipnote_id.id), + Err(err) => handle_flipnote_id_error(err), + }, + Err(err) => handle_file_read_error(err, &cli.file), + }, + Actions::Check => match read(&cli.file) { + Ok(bin_data) => match extract_id_with_checksum(&bin_data) { + Ok(flipnote_id) => { + let valid_checksum = compute_checksum(&bin_data).unwrap(); + + println!("Flipnote Studio ID: {:016X}", flipnote_id.id); + println!( + "Checksum: {:04X} ({})", + flipnote_id.checksum, + if flipnote_id.checksum == valid_checksum { + String::from("valid") + } else { + format!("invalid; expect {:04X}", valid_checksum) + } + ); + } + Err(err) => handle_flipnote_id_error(err), + }, + Err(err) => handle_file_read_error(err, &cli.file), + }, + } +}