├── .github ├── dependabot.yml └── workflows │ ├── cancel-stale.yml │ └── main.yml ├── .gitignore ├── .rustfmt.toml ├── .taplo.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── codecov.yml ├── example ├── Cargo.toml ├── assets │ ├── branding │ │ └── bevy_logo_dark_big.png │ ├── fonts │ │ └── FiraSans-Bold.ttf │ └── nonascii │ │ └── 图 │ │ └── 图.png ├── build.rs └── src │ ├── main.rs │ └── mod.rs ├── src ├── bundler │ ├── asset_bundler.rs │ └── mod.rs ├── lib.rs ├── options │ ├── bundled_asset_options.rs │ └── mod.rs └── plugin │ ├── bundled_asset_io.rs │ ├── bundled_asset_plugin.rs │ ├── mod.rs │ └── path_info.rs └── tests ├── bundler.rs ├── common.rs ├── e2e.rs └── plugin.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" # Location of package manifests 8 | open-pull-requests-limit: 0 9 | schedule: 10 | interval: "weekly" 11 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot#enabling-dependabot-version-updates-for-actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | open-pull-requests-limit: 1 15 | schedule: 16 | interval: "weekly" 17 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates#example-disabling-version-updates-for-some-dependencies 18 | # ignore: 19 | -------------------------------------------------------------------------------- /.github/workflows/cancel-stale.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/cancel-workflow-action#advanced-pull-requests-from-forks 2 | name: Cancel stale workflows 3 | on: 4 | workflow_run: 5 | workflows: ["main"] 6 | types: 7 | - requested 8 | jobs: 9 | cancel: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: styfle/cancel-workflow-action@0.11.0 13 | with: 14 | workflow_id: ${{ github.event.workflow.id }} 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [main, bevy_main] 6 | pull_request: 7 | branches: [main, bevy_main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | steps: 19 | - name: Install latest stable 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | override: true 24 | components: rustfmt, clippy 25 | - uses: actions/checkout@v3 26 | 27 | - uses: Swatinem/rust-cache@v2 28 | 29 | - run: cargo check --release --no-default-features 30 | 31 | - run: cargo check --release 32 | - run: cargo test --release 33 | 34 | - run: cargo check --release --all-features 35 | - run: cargo test --release --all-features 36 | 37 | - run: cargo fmt --all -- --check 38 | 39 | - run: cargo clippy --release -- --deny warnings 40 | if: matrix.os != 'windows-latest' 41 | - run: cargo clippy --release --workspace -- --deny warnings 42 | if: matrix.os == 'windows-latest' 43 | 44 | - run: cargo build --release -p example 45 | if: matrix.os == 'windows-latest' 46 | 47 | - uses: actions/upload-artifact@v3 48 | if: matrix.os == 'windows-latest' 49 | with: 50 | name: example-windows 51 | path: | 52 | target/release/*.exe 53 | target/release/*.bin 54 | if-no-files-found: error 55 | codecov: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: actions-rs/toolchain@v1 60 | with: 61 | profile: minimal 62 | toolchain: stable 63 | override: true 64 | components: llvm-tools-preview 65 | - uses: Swatinem/rust-cache@v2 66 | - uses: taiki-e/install-action@cargo-llvm-cov 67 | - run: cargo llvm-cov --all-features --lcov --output-path lcov.info 68 | - uses: actions/upload-artifact@v3 69 | with: 70 | name: lcov.info 71 | path: lcov.info 72 | if-no-files-found: error 73 | - name: Upload to codecov 74 | run: | 75 | curl -Os https://uploader.codecov.io/latest/linux/codecov 76 | chmod +x codecov 77 | ./codecov -f lcov.info -Z 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/rust,osx,visualstudiocode,intellij+all 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=rust,osx,visualstudiocode,intellij+all 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | # Editor-based Rest Client 75 | .idea/httpRequests 76 | 77 | # Android studio 3.1+ serialized cache file 78 | .idea/caches/build_file_checksums.ser 79 | 80 | ### Intellij+all Patch ### 81 | # Ignores the whole .idea folder and all .iml files 82 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 83 | 84 | .idea/ 85 | 86 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 87 | 88 | *.iml 89 | modules.xml 90 | .idea/misc.xml 91 | *.ipr 92 | 93 | # Sonarlint plugin 94 | .idea/sonarlint 95 | 96 | ### OSX ### 97 | # General 98 | .DS_Store 99 | .AppleDouble 100 | .LSOverride 101 | 102 | # Icon must end with two \r 103 | Icon 104 | 105 | # Thumbnails 106 | ._* 107 | 108 | # Files that might appear in the root of a volume 109 | .DocumentRevisions-V100 110 | .fseventsd 111 | .Spotlight-V100 112 | .TemporaryItems 113 | .Trashes 114 | .VolumeIcon.icns 115 | .com.apple.timemachine.donotpresent 116 | 117 | # Directories potentially created on remote AFP share 118 | .AppleDB 119 | .AppleDesktop 120 | Network Trash Folder 121 | Temporary Items 122 | .apdisk 123 | 124 | ### Rust ### 125 | # Generated by Cargo 126 | # will have compiled files and executables 127 | debug/ 128 | target/ 129 | 130 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 131 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 132 | Cargo.lock 133 | 134 | # These are backup files generated by rustfmt 135 | **/*.rs.bk 136 | 137 | # MSVC Windows builds of rustc generate these, which store debugging information 138 | *.pdb 139 | 140 | ### VisualStudioCode ### 141 | .vscode/* 142 | !.vscode/settings.json 143 | !.vscode/tasks.json 144 | !.vscode/launch.json 145 | !.vscode/extensions.json 146 | *.code-workspace 147 | 148 | # Local History for Visual Studio Code 149 | .history/ 150 | 151 | ### VisualStudioCode Patch ### 152 | # Ignore all local history of files 153 | .history 154 | .ionide 155 | 156 | # End of https://www.toptal.com/developers/gitignore/api/rust,osx,visualstudiocode,intellij+all 157 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Doc: https://rust-lang.github.io/rustfmt/ 2 | imports_granularity = "Crate" 3 | group_imports = "StdExternalCrate" 4 | hex_literal_case = "Lower" 5 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | [[rule]] 2 | keys = [ 3 | "dependencies", 4 | "dev-dependencies", 5 | "build-dependencies", 6 | "workspace.dependencies", 7 | ] 8 | 9 | [rule.formatting] 10 | reorder_keys = true 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_assets_bundler" 3 | version = "0.6.0" 4 | 5 | description = "Assets Bundler for bevy, with content encryption support." 6 | 7 | categories = ["cryptography", "encoding", "game-development"] 8 | keywords = ["bevy", "asset", "assets", "bundler"] 9 | 10 | readme = "README.md" 11 | repository = "https://github.com/hanabi1224/bevy_assets_bundler" 12 | 13 | edition = "2021" 14 | exclude = [".github", ".vscode", "*.yml"] 15 | license = "MIT" 16 | 17 | [workspace] 18 | exclude = [] 19 | members = ["example"] 20 | 21 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 22 | 23 | [features] 24 | default = ["encryption"] 25 | 26 | encryption = ["aes", "block-modes"] 27 | 28 | # compression = ["brotli"] 29 | 30 | [workspace.dependencies] 31 | bevy = { version = "0.10", default-features = false } 32 | 33 | [dependencies] 34 | bevy = { workspace = true, default-features = false, features = ["bevy_asset"] } 35 | 36 | anyhow = "1" 37 | bs58 = "0.4" 38 | cfg-if = "1" 39 | tar = "0.4.38" 40 | tracing = "0.1" 41 | 42 | # encryption 43 | aes = { version = "0.7", optional = true } 44 | block-modes = { version = "0.8", optional = true } 45 | 46 | # compression 47 | # brotli = {version = "3.3", optional = true} 48 | 49 | [dev-dependencies] 50 | futures-lite = "1" 51 | rand = "0" 52 | uuid = "1" 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 hanabi1224 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bevy Assets Bundler 2 | 3 | [![github action](https://github.com/hanabi1224/bevy_assets_bundler/actions/workflows/main.yml/badge.svg)](https://github.com/hanabi1224/bevy_assets_bundler/actions/workflows/main.yml) 4 | [![codecov](https://codecov.io/gh/hanabi1224/bevy_assets_bundler/branch/main/graph/badge.svg?token=gOcqVpMmIY)](https://codecov.io/gh/hanabi1224/bevy_assets_bundler) 5 | [![dependency status](https://deps.rs/repo/github/hanabi1224/bevy_assets_bundler/status.svg?style=flat-square)](https://deps.rs/repo/github/hanabi1224/bevy_assets_bundler) 6 | [![loc](https://tokei.rs/b1/github/hanabi1224/bevy_assets_bundler?category=code)](https://github.com/hanabi1224/bevy_assets_bundler) 7 | [![License](https://img.shields.io/github/license/hanabi1224/bevy_assets_bundler.svg)](https://github.com/hanabi1224/bevy_assets_bundler/blob/master/LICENSE) 8 | 9 | [![crates.io](https://img.shields.io/crates/v/bevy_assets_bundler)](https://crates.io/crates/bevy_assets_bundler) 10 | [![docs.rs](https://docs.rs/bevy_assets_bundler/badge.svg)](https://docs.rs/bevy_assets_bundler) 11 | 12 | Assets Bundler for bevy, with content encryption support. Current archive format is tar and encryption algorithm is AES 13 | 14 | ## Features 15 | 16 | - Bundle asset folder into a single assets.bin file 17 | - Asset encryption with custom key 18 | - Asset file names encoding (base58 when ecryption is off, AES+base58 otherwise) 19 | - One simple switch to turn off bundling on debug build 20 | 21 | ## [Installation](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/example/Cargo.toml) 22 | ```toml 23 | # Cargo.toml 24 | [dependencies] 25 | bevy = "0.9" 26 | bevy_assets_bundler = "0.5" 27 | 28 | [build-dependencies] 29 | bevy_assets_bundler = "0.5" 30 | ``` 31 | 32 | ## [Build Script](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/example/build.rs) 33 | 34 | You can generate a random key with [this playground](https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=cd3cb4ca8b86e67070b94caf366d162e) 35 | 36 | ```rust 37 | use bevy_assets_bundler::*; 38 | 39 | // build.rs 40 | // encryption key: [u8; 16] array 41 | // make sure the key is consistent between build.rs and main.rs 42 | // or follow the example code to share code between build.rs and main.rs 43 | fn main() { 44 | let key = [30, 168, 132, 180, 250, 203, 124, 96, 221, 206, 64, 239, 102, 20, 139, 79]; 45 | let mut options = AssetBundlingOptions::default(); 46 | options.set_encryption_key(key); 47 | options.encode_file_names = true; 48 | options.enabled_on_debug_build = true; 49 | AssetBundler::from(options).build();//.unwrap(); 50 | } 51 | ``` 52 | 53 | ## [Bevy Setup](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/example/src/main.rs) 54 | ```rust 55 | use bevy_assets_bundler::*; 56 | use bevy::{asset::AssetPlugin, prelude::*}; 57 | 58 | fn main() { 59 | // encryption key: [u8; 16] array 60 | // make sure the key is consistent between build.rs and main.rs 61 | // or follow the example code to share code between build.rs and main.rs 62 | let key = [30, 168, 132, 180, 250, 203, 124, 96, 221, 206, 64, 239, 102, 20, 139, 79]; 63 | let mut options = AssetBundlingOptions::default(); 64 | options.set_encryption_key(key); 65 | options.encode_file_names = true; 66 | options.enabled_on_debug_build = true; 67 | 68 | App::new() 69 | .add_plugins( 70 | DefaultPlugins 71 | .build() 72 | .add_before::(BundledAssetIoPlugin::from( 73 | options, 74 | )), 75 | ) 76 | .add_startup_system(setup) 77 | .run(); 78 | } 79 | 80 | fn setup(mut commands: Commands, asset_server: Res) { 81 | } 82 | ``` 83 | 84 | ## [Options](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/src/plugin/bundled_asset_options.rs) 85 | ```rust 86 | #[derive(Debug, Clone)] 87 | pub struct AssetBundlingOptions { 88 | #[cfg(feature = "encryption")] 89 | pub encryption_on: bool, 90 | #[cfg(feature = "encryption")] 91 | pub encryption_key: Option<[u8; 16]>, 92 | #[cfg(feature = "compression")] 93 | pub enable_compression: bool, 94 | pub enabled_on_debug_build: bool, 95 | pub encode_file_names: bool, 96 | pub asset_bundle_name: String, 97 | } 98 | ``` 99 | 100 | ## TODO 101 | 102 | - Compression 103 | - More encryption algorithms 104 | 105 | ## Bevy Version Supported 106 | 107 | |bevy|bevy_assets_bundler| 108 | |---|---| 109 | |main|bevy_main| 110 | |0.10|0.6| 111 | |0.9|0.5| 112 | |0.8|0.4| 113 | |0.7|0.3| 114 | |0.6|0.2| 115 | |0.5|0.1| 116 | 117 | ## Examples 118 | 119 | Check out [example](https://github.com/hanabi1224/bevy_assets_bundler/tree/main/example) and [E2E test](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/tests/e2e.rs) 120 | 121 | To run example: ```cargo run -p example``` 122 | 123 | go to ```target/release``` folder, now you can move example(.exe) and assets.bin to some other place and run, just keep the relative path between them. 124 | 125 | ## Disclaimer 126 | 127 | The encryption mechnism this library provides does not protect your assets from **ALL** kinds of reverse engineering as long as the game executable and the assets bundle are distributed to end users. 128 | 129 | ## [License](https://github.com/hanabi1224/bevy_assets_bundler/blob/main/LICENSE) 130 | 131 | MIT 132 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,files,footer" 19 | behavior: default 20 | require_changes: no 21 | 22 | ignore: 23 | - "tests" 24 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "example" 4 | version = "0.1.0" 5 | 6 | publish = false 7 | 8 | [dependencies] 9 | bevy = { workspace = true, features = ["default"] } 10 | lazy_static = "1" 11 | 12 | bevy_assets_bundler = { path = "../" } 13 | 14 | [dev-dependencies] 15 | 16 | [build-dependencies] 17 | bevy_assets_bundler = { path = "../" } 18 | lazy_static = "1" 19 | 20 | [profile.release] 21 | strip = true 22 | -------------------------------------------------------------------------------- /example/assets/branding/bevy_logo_dark_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanabi1224/bevy_assets_bundler/cd95e9c02cd2b67d23cda2b0e5bc846b6c61f289/example/assets/branding/bevy_logo_dark_big.png -------------------------------------------------------------------------------- /example/assets/fonts/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanabi1224/bevy_assets_bundler/cd95e9c02cd2b67d23cda2b0e5bc846b6c61f289/example/assets/fonts/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /example/assets/nonascii/图/图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanabi1224/bevy_assets_bundler/cd95e9c02cd2b67d23cda2b0e5bc846b6c61f289/example/assets/nonascii/图/图.png -------------------------------------------------------------------------------- /example/build.rs: -------------------------------------------------------------------------------- 1 | mod src; 2 | use bevy_assets_bundler::*; 3 | use src::BUNDLE_OPTIONS; 4 | 5 | fn main() { 6 | AssetBundler::from(BUNDLE_OPTIONS.clone()).build().unwrap(); 7 | } 8 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | mod r#mod; 2 | use std::env; 3 | 4 | use bevy::{ 5 | log::{Level, LogPlugin}, 6 | prelude::*, 7 | }; 8 | use bevy_assets_bundler::BundledAssetIoPlugin; 9 | use r#mod::BUNDLE_OPTIONS; 10 | 11 | /// This example illustrates the various features of Bevy UI. 12 | fn main() { 13 | println!("cwd: {:?}", env::current_dir()); 14 | App::new() 15 | .add_plugins( 16 | DefaultPlugins 17 | .set(LogPlugin { 18 | level: Level::INFO, 19 | ..Default::default() 20 | }) 21 | .build() 22 | // the custom asset io plugin must be inserted in-between the 23 | // `CorePlugin' and `AssetPlugin`. It needs to be after the 24 | // CorePlugin, so that the IO task pool has already been constructed. 25 | // And it must be before the `AssetPlugin` so that the asset plugin 26 | // doesn't create another instance of an asset server. In general, 27 | // the AssetPlugin should still run so that other aspects of the 28 | // asset system are initialized correctly. 29 | .add_before::(BundledAssetIoPlugin::from( 30 | BUNDLE_OPTIONS.clone(), 31 | )), 32 | ) 33 | .add_startup_system(setup) 34 | .run(); 35 | } 36 | 37 | fn setup(mut commands: Commands, asset_server: Res) { 38 | // ui camera 39 | commands.spawn(Camera2dBundle::default()); 40 | // root node 41 | commands 42 | .spawn(NodeBundle { 43 | style: Style { 44 | size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), 45 | justify_content: JustifyContent::SpaceBetween, 46 | ..Default::default() 47 | }, 48 | background_color: Color::NONE.into(), 49 | ..Default::default() 50 | }) 51 | .with_children(|parent| { 52 | // left vertical fill (border) 53 | parent 54 | .spawn(NodeBundle { 55 | style: Style { 56 | size: Size::new(Val::Px(200.0), Val::Percent(100.0)), 57 | border: UiRect::all(Val::Px(2.0)), 58 | ..Default::default() 59 | }, 60 | background_color: Color::rgb(0.65, 0.65, 0.65).into(), 61 | ..Default::default() 62 | }) 63 | .with_children(|parent| { 64 | // left vertical fill (content) 65 | parent 66 | .spawn(NodeBundle { 67 | style: Style { 68 | size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), 69 | align_items: AlignItems::FlexEnd, 70 | ..Default::default() 71 | }, 72 | background_color: Color::rgb(0.15, 0.15, 0.15).into(), 73 | ..Default::default() 74 | }) 75 | .with_children(|parent| { 76 | // text 77 | parent.spawn(TextBundle { 78 | style: Style { 79 | margin: UiRect::all(Val::Px(5.0)), 80 | ..Default::default() 81 | }, 82 | text: Text::from_section( 83 | "Text Example", 84 | TextStyle { 85 | font: asset_server.load("fonts/FiraSans-Bold.ttf"), 86 | font_size: 30.0, 87 | color: Color::WHITE, 88 | }, 89 | ), 90 | ..Default::default() 91 | }); 92 | }); 93 | }); 94 | // right vertical fill 95 | parent.spawn(NodeBundle { 96 | style: Style { 97 | size: Size::new(Val::Px(200.0), Val::Percent(100.0)), 98 | ..Default::default() 99 | }, 100 | background_color: Color::rgb(0.15, 0.15, 0.15).into(), 101 | ..Default::default() 102 | }); 103 | // absolute positioning 104 | parent 105 | .spawn(NodeBundle { 106 | style: Style { 107 | size: Size::new(Val::Px(200.0), Val::Px(200.0)), 108 | position_type: PositionType::Absolute, 109 | position: UiRect { 110 | left: Val::Px(210.0), 111 | bottom: Val::Px(10.0), 112 | ..Default::default() 113 | }, 114 | border: UiRect::all(Val::Px(20.0)), 115 | ..Default::default() 116 | }, 117 | background_color: Color::rgb(0.4, 0.4, 1.0).into(), 118 | ..Default::default() 119 | }) 120 | .with_children(|parent| { 121 | parent.spawn(NodeBundle { 122 | style: Style { 123 | size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), 124 | ..Default::default() 125 | }, 126 | background_color: Color::rgb(0.8, 0.8, 1.0).into(), 127 | ..Default::default() 128 | }); 129 | }); 130 | // render order test: reddest in the back, whitest in the front (flex center) 131 | parent 132 | .spawn(NodeBundle { 133 | style: Style { 134 | size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), 135 | position_type: PositionType::Absolute, 136 | align_items: AlignItems::Center, 137 | justify_content: JustifyContent::Center, 138 | ..Default::default() 139 | }, 140 | background_color: Color::NONE.into(), 141 | ..Default::default() 142 | }) 143 | .with_children(|parent| { 144 | parent 145 | .spawn(NodeBundle { 146 | style: Style { 147 | size: Size::new(Val::Px(100.0), Val::Px(100.0)), 148 | ..Default::default() 149 | }, 150 | background_color: Color::rgb(1.0, 0.0, 0.0).into(), 151 | ..Default::default() 152 | }) 153 | .with_children(|parent| { 154 | parent.spawn(NodeBundle { 155 | style: Style { 156 | size: Size::new(Val::Px(100.0), Val::Px(100.0)), 157 | position_type: PositionType::Absolute, 158 | position: UiRect { 159 | left: Val::Px(20.0), 160 | bottom: Val::Px(20.0), 161 | ..Default::default() 162 | }, 163 | ..Default::default() 164 | }, 165 | background_color: Color::rgb(1.0, 0.3, 0.3).into(), 166 | ..Default::default() 167 | }); 168 | parent.spawn(NodeBundle { 169 | style: Style { 170 | size: Size::new(Val::Px(100.0), Val::Px(100.0)), 171 | position_type: PositionType::Absolute, 172 | position: UiRect { 173 | left: Val::Px(40.0), 174 | bottom: Val::Px(40.0), 175 | ..Default::default() 176 | }, 177 | ..Default::default() 178 | }, 179 | background_color: Color::rgb(1.0, 0.5, 0.5).into(), 180 | ..Default::default() 181 | }); 182 | parent.spawn(NodeBundle { 183 | style: Style { 184 | size: Size::new(Val::Px(100.0), Val::Px(100.0)), 185 | position_type: PositionType::Absolute, 186 | position: UiRect { 187 | left: Val::Px(60.0), 188 | bottom: Val::Px(60.0), 189 | ..Default::default() 190 | }, 191 | ..Default::default() 192 | }, 193 | background_color: Color::rgb(1.0, 0.7, 0.7).into(), 194 | ..Default::default() 195 | }); 196 | // alpha test 197 | parent.spawn(NodeBundle { 198 | style: Style { 199 | size: Size::new(Val::Px(100.0), Val::Px(100.0)), 200 | position_type: PositionType::Absolute, 201 | position: UiRect { 202 | left: Val::Px(80.0), 203 | bottom: Val::Px(80.0), 204 | ..Default::default() 205 | }, 206 | ..Default::default() 207 | }, 208 | background_color: Color::rgba(1.0, 0.9, 0.9, 0.4).into(), 209 | ..Default::default() 210 | }); 211 | }); 212 | }); 213 | // bevy logo (flex center) 214 | parent 215 | .spawn(NodeBundle { 216 | style: Style { 217 | size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), 218 | position_type: PositionType::Absolute, 219 | justify_content: JustifyContent::Center, 220 | align_items: AlignItems::FlexEnd, 221 | ..Default::default() 222 | }, 223 | background_color: Color::NONE.into(), 224 | ..Default::default() 225 | }) 226 | .with_children(|parent| { 227 | // bevy logo (image) 228 | parent.spawn(ImageBundle { 229 | style: Style { 230 | size: Size::new(Val::Px(500.0), Val::Auto), 231 | ..Default::default() 232 | }, 233 | image: asset_server.load("nonascii/图/图.png").into(), 234 | ..Default::default() 235 | }); 236 | }); 237 | }); 238 | } 239 | -------------------------------------------------------------------------------- /example/src/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy_assets_bundler::AssetBundlingOptions; 2 | 3 | lazy_static::lazy_static! { 4 | pub static ref BUNDLE_OPTIONS: AssetBundlingOptions = { 5 | let key = [30, 168, 132, 180, 250, 203, 124, 96, 221, 206, 64, 239, 102, 20, 139, 79]; 6 | let mut options = AssetBundlingOptions::default(); 7 | options.set_encryption_key(key); 8 | options.enabled_on_debug_build = true; 9 | options.encode_file_names = true; 10 | // options.asset_bundle_name = "a/assets.bin".into(); 11 | options 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/bundler/asset_bundler.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "encryption")] 2 | use std::io::Read; 3 | use std::{ 4 | env, fs, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use crate::AssetBundlingOptions; 9 | 10 | pub struct AssetBundler { 11 | pub options: AssetBundlingOptions, 12 | pub asset_folder: String, 13 | } 14 | 15 | impl Default for AssetBundler { 16 | fn default() -> Self { 17 | Self { 18 | options: AssetBundlingOptions::default(), 19 | asset_folder: crate::DEFAULT_ASSET_FOLDER.to_owned(), 20 | } 21 | } 22 | } 23 | 24 | impl From for AssetBundler { 25 | fn from(options: AssetBundlingOptions) -> Self { 26 | Self { 27 | options, 28 | asset_folder: crate::DEFAULT_ASSET_FOLDER.to_owned(), 29 | } 30 | } 31 | } 32 | 33 | impl AssetBundler { 34 | pub fn with_asset_folder(&mut self, path: impl Into) -> &mut Self { 35 | self.asset_folder = path.into(); 36 | self 37 | } 38 | 39 | pub fn build(&self) -> anyhow::Result<()> { 40 | if !self.options.enabled_on_debug_build 41 | && env::var("PROFILE").unwrap_or_else(|_| "".into()) == "debug" 42 | { 43 | warn!("disabled on debug build"); 44 | return Ok(()); 45 | } 46 | info!("Start bundling assets, cwd: {:?}", env::current_dir()); 47 | 48 | #[cfg(feature = "encryption")] 49 | if self.options.encryption_on && self.options.encryption_key.is_none() { 50 | // Default key? 51 | return Err(anyhow::Error::msg( 52 | "Asset encryption is enabled but encryption key is not provided.", 53 | )); 54 | } 55 | 56 | let asset_dir = PathBuf::from(&self.asset_folder); 57 | if asset_dir.is_dir() { 58 | let exe_dir = get_exe_dir()?; 59 | let bundle_file_path = exe_dir.join(self.options.asset_bundle_name.clone()); 60 | // Create bundle_path parent dir if not exist. 61 | if let Some(bundle_file_dir) = bundle_file_path.parent() { 62 | if !bundle_file_dir.exists() { 63 | fs::create_dir_all(bundle_file_dir)?; 64 | } 65 | } 66 | 67 | let tar_file = fs::File::create(bundle_file_path)?; 68 | let mut tar_builder = tar::Builder::new(tar_file); 69 | archive_dir(&mut tar_builder, &asset_dir, &self.options)?; 70 | Ok(()) 71 | } else { 72 | Err(anyhow::Error::msg(format!( 73 | "Asset folder not found: {}, cwd: {:?}", 74 | self.asset_folder, 75 | env::current_dir()? 76 | ))) 77 | } 78 | } 79 | } 80 | 81 | fn archive_dir( 82 | builder: &mut tar::Builder, 83 | asset_dir: &Path, 84 | options: &AssetBundlingOptions, 85 | ) -> anyhow::Result<()> { 86 | archive_dir_recursive(builder, asset_dir, asset_dir, options)?; 87 | Ok(()) 88 | } 89 | 90 | fn archive_dir_recursive( 91 | builder: &mut tar::Builder, 92 | dir: &Path, 93 | prefix: &Path, 94 | options: &AssetBundlingOptions, 95 | ) -> anyhow::Result<()> { 96 | for entry_result in fs::read_dir(dir)? { 97 | let entry_path = entry_result?.path(); 98 | if entry_path.is_dir() { 99 | archive_dir_recursive(builder, &entry_path, prefix, options)?; 100 | } else { 101 | let mut name_in_archive = entry_path.strip_prefix(prefix)?.to_owned(); 102 | if options.encode_file_names { 103 | name_in_archive = options.try_encode_path(&name_in_archive)?; 104 | } 105 | let mut file = fs::File::open(entry_path.clone())?; 106 | #[cfg(feature = "encryption")] 107 | if options.is_encryption_ready() { 108 | let mut plain = Vec::new(); 109 | file.read_to_end(&mut plain)?; 110 | if let Some(encrypted) = options.try_encrypt(&plain)? { 111 | let mut header = tar::Header::new_gnu(); 112 | header.set_path(name_in_archive)?; 113 | let metadata = fs::metadata(&entry_path)?; 114 | header.set_metadata(&metadata); 115 | header.set_size(encrypted.len() as u64); 116 | // header.set_mode(0o400); 117 | header.set_cksum(); 118 | builder.append(&header, encrypted.as_slice())?; 119 | continue; 120 | } 121 | } 122 | 123 | builder.append_file(name_in_archive, &mut file)?; 124 | } 125 | } 126 | Ok(()) 127 | } 128 | 129 | fn get_exe_dir() -> anyhow::Result { 130 | let mut dir = env::current_exe()?; 131 | dir.pop(); 132 | if !env::var("OUT_DIR").unwrap_or_else(|_| "".into()).is_empty() { 133 | dir.pop(); 134 | dir.pop(); 135 | } 136 | Ok(dir) 137 | } 138 | -------------------------------------------------------------------------------- /src/bundler/mod.rs: -------------------------------------------------------------------------------- 1 | mod asset_bundler; 2 | 3 | pub use asset_bundler::AssetBundler; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | #[macro_use] 4 | extern crate tracing; 5 | 6 | mod options; 7 | pub use options::AssetBundlingOptions; 8 | 9 | mod bundler; 10 | pub use bundler::AssetBundler; 11 | 12 | mod plugin; 13 | #[cfg(feature = "encryption")] 14 | use aes::Aes128; 15 | pub use bevy::asset::AssetIo; 16 | #[cfg(feature = "encryption")] 17 | use block_modes::{block_padding::Pkcs7, BlockMode, Cbc}; 18 | pub use plugin::{BundledAssetIo, BundledAssetIoPlugin}; 19 | 20 | #[cfg(feature = "encryption")] 21 | type Aes128Cbc = Cbc; 22 | 23 | const DEFAULT_ASSET_FOLDER: &str = "assets"; 24 | const DEFAULT_ASSET_BUNDLE_NAME: &str = "assets.bin"; 25 | -------------------------------------------------------------------------------- /src/options/bundled_asset_options.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | #[cfg(feature = "encryption")] 4 | use crate::{Aes128Cbc, BlockMode}; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct AssetBundlingOptions { 8 | #[cfg(feature = "encryption")] 9 | pub encryption_on: bool, 10 | #[cfg(feature = "encryption")] 11 | pub encryption_key: Option<[u8; 16]>, 12 | #[cfg(feature = "compression")] 13 | pub enable_compression: bool, 14 | pub enabled_on_debug_build: bool, 15 | pub encode_file_names: bool, 16 | pub asset_bundle_name: String, 17 | } 18 | 19 | impl Default for AssetBundlingOptions { 20 | fn default() -> Self { 21 | Self { 22 | #[cfg(feature = "encryption")] 23 | encryption_on: false, 24 | #[cfg(feature = "encryption")] 25 | encryption_key: None, 26 | enabled_on_debug_build: false, 27 | #[cfg(feature = "compression")] 28 | enable_compression: false, 29 | encode_file_names: false, 30 | asset_bundle_name: crate::DEFAULT_ASSET_BUNDLE_NAME.to_owned(), 31 | } 32 | } 33 | } 34 | 35 | impl AssetBundlingOptions { 36 | #[cfg(feature = "encryption")] 37 | pub fn set_encryption_key(&mut self, key: [u8; 16]) -> &mut Self { 38 | self.encryption_on = true; 39 | self.encryption_key = Some(key); 40 | self 41 | } 42 | 43 | #[cfg(feature = "encryption")] 44 | pub(crate) fn is_encryption_ready(&self) -> bool { 45 | self.encryption_on && self.encryption_key.is_some() 46 | } 47 | 48 | #[cfg(feature = "encryption")] 49 | pub(crate) fn try_get_cipher_if_needed(&self) -> anyhow::Result> { 50 | if self.encryption_on { 51 | if let Some(aes_key) = self.encryption_key { 52 | return Ok(Some(Aes128Cbc::new_from_slices(&aes_key, &aes_key)?)); 53 | } 54 | } 55 | Ok(None) 56 | } 57 | 58 | #[cfg(feature = "encryption")] 59 | pub(crate) fn try_encrypt(&self, plain: &[u8]) -> anyhow::Result>> { 60 | if let Some(cipher) = self.try_get_cipher_if_needed()? { 61 | return Ok(Some(cipher.encrypt_vec(plain))); 62 | } 63 | Ok(None) 64 | } 65 | 66 | #[cfg(feature = "encryption")] 67 | pub(crate) fn try_decrypt(&self, encrypted: &[u8]) -> anyhow::Result>> { 68 | if let Some(cipher) = self.try_get_cipher_if_needed()? { 69 | return Ok(Some(cipher.decrypt_vec(encrypted)?)); 70 | } 71 | Ok(None) 72 | } 73 | 74 | fn try_encode_string(&self, s: &str) -> anyhow::Result { 75 | #[cfg(feature = "encryption")] 76 | if self.is_encryption_ready() { 77 | let bytes = s.as_bytes(); 78 | if let Some(encrypted) = self.try_encrypt(bytes)? { 79 | return Ok(bs58::encode(encrypted).into_string()); 80 | } 81 | } 82 | 83 | Ok(bs58::encode(s).into_string()) 84 | } 85 | 86 | fn try_decode_string(&self, s: &str) -> anyhow::Result { 87 | let vec = bs58::decode(s).into_vec()?; 88 | #[cfg(feature = "encryption")] 89 | if self.is_encryption_ready() { 90 | if let Some(decrypted) = self.try_decrypt(&vec)? { 91 | return Ok(String::from_utf8(decrypted)?); 92 | } 93 | } 94 | 95 | Ok(String::from_utf8(vec)?) 96 | } 97 | 98 | pub(crate) fn try_encode_path(&self, p: &Path) -> anyhow::Result { 99 | Ok(p.to_str() 100 | .unwrap() 101 | .replace('\\', "/") 102 | .split('/') 103 | .map(|part| self.try_encode_string(part).unwrap()) 104 | .collect()) 105 | } 106 | 107 | pub(crate) fn try_decode_path(&self, p: &Path) -> anyhow::Result { 108 | Ok(p.to_str() 109 | .unwrap() 110 | .replace('\\', "/") 111 | .split('/') 112 | .map(|part| self.try_decode_string(part).unwrap()) 113 | .collect()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/options/mod.rs: -------------------------------------------------------------------------------- 1 | mod bundled_asset_options; 2 | pub use bundled_asset_options::AssetBundlingOptions; 3 | -------------------------------------------------------------------------------- /src/plugin/bundled_asset_io.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Borrow, 3 | collections::HashMap, 4 | env, 5 | fs::File, 6 | io::prelude::*, 7 | path::{Path, PathBuf}, 8 | sync::{Arc, RwLock}, 9 | }; 10 | 11 | use bevy::{ 12 | asset::{AssetIo, AssetIoError}, 13 | utils::BoxedFuture, 14 | }; 15 | use tar::Archive; 16 | 17 | use super::path_info::ArchivePathInfo; 18 | use crate::AssetBundlingOptions; 19 | 20 | type ParentDirToPathInfo = HashMap>; 21 | 22 | #[derive(Default)] 23 | pub struct BundledAssetIo { 24 | options: AssetBundlingOptions, 25 | parent_dir_to_path_info: Option>>, 26 | } 27 | 28 | impl From for BundledAssetIo { 29 | fn from(options: AssetBundlingOptions) -> Self { 30 | Self { 31 | options, 32 | parent_dir_to_path_info: None, 33 | } 34 | } 35 | } 36 | 37 | impl BundledAssetIo { 38 | pub fn ensure_loaded(&mut self) -> anyhow::Result<()> { 39 | if self.parent_dir_to_path_info.is_none() { 40 | let bundle_path = self.get_bundle_path()?; 41 | info!("Loading asset bundle: {:?}", bundle_path); 42 | let file = File::open(bundle_path)?; 43 | let mut archive = Archive::new(file); 44 | let mut mappings: ParentDirToPathInfo = HashMap::new(); 45 | let mut n_entries = 0; 46 | for entry in archive.entries()?.flatten() { 47 | n_entries += 1; 48 | let path = entry.path()?; 49 | let decoded_path = if self.options.encode_file_names { 50 | self.options.try_decode_path(path.borrow())? 51 | // PathBuf::from(self.options.try_decode_string(path.to_str().unwrap())?) 52 | } else { 53 | path.to_path_buf() 54 | }; 55 | // let is_dir = path.is_dir(); 56 | let mut parent_dir = decoded_path.clone(); 57 | let parent_dir_str = if parent_dir.pop() { 58 | normalize_path(&parent_dir) 59 | } else { 60 | "".into() 61 | }; 62 | debug!("Loading asset file {:?}, dir:{:?}", path, parent_dir); 63 | let path_info = ArchivePathInfo::new(decoded_path); 64 | if let Some(vec) = mappings.get_mut(&parent_dir_str) { 65 | vec.push(path_info); 66 | } else { 67 | mappings.insert(parent_dir_str, vec![path_info]); 68 | } 69 | } 70 | info!("{} asset files loaded.", n_entries); 71 | self.parent_dir_to_path_info = Some(Arc::new(RwLock::new(mappings))); 72 | Ok(()) 73 | } else { 74 | Err(anyhow::Error::msg("Entity file is not found")) 75 | } 76 | } 77 | 78 | fn get_bundle_path(&self) -> anyhow::Result { 79 | let mut bundle_path = env::current_exe().map_err(AssetIoError::Io)?; 80 | bundle_path.pop(); 81 | bundle_path.push(self.options.asset_bundle_name.clone()); 82 | Ok(bundle_path) 83 | } 84 | } 85 | 86 | impl AssetIo for BundledAssetIo { 87 | fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result, AssetIoError>> { 88 | info!("load_path: {:?}", path); 89 | Box::pin(async move { 90 | let bundle_path = self.get_bundle_path()?; 91 | let file = File::open(bundle_path)?; 92 | let encoded_entry_path = if self.options.encode_file_names { 93 | self.options.try_encode_path(path).map_err(map_error)? 94 | } else { 95 | PathBuf::from(normalize_path(path)) 96 | }; 97 | let mut archive = Archive::new(file); 98 | for mut entry in archive.entries()?.flatten() { 99 | let entry_path = entry.path()?; 100 | if entry_path.eq(&encoded_entry_path) { 101 | let mut vec = Vec::new(); 102 | entry.read_to_end(&mut vec)?; 103 | #[cfg(feature = "encryption")] 104 | if let Some(decrypted) = self.options.try_decrypt(&vec).map_err(map_error)? { 105 | return Ok(decrypted); 106 | } 107 | return Ok(vec); 108 | } 109 | } 110 | Err(AssetIoError::NotFound(path.to_path_buf())) 111 | }) 112 | } 113 | 114 | fn read_directory( 115 | &self, 116 | path: &Path, 117 | ) -> Result>, AssetIoError> { 118 | info!("[read_directory] {:?}", path); 119 | if let Some(lock) = self.parent_dir_to_path_info.clone() { 120 | let mappings = lock.read().unwrap(); 121 | let path_str = normalize_path(path); 122 | if let Some(entries) = mappings.get(&path_str) { 123 | #[allow(clippy::needless_collect)] 124 | let vec: Vec<_> = entries.iter().map(|e| e.path()).collect(); 125 | return Ok(Box::new(vec.into_iter())); 126 | } 127 | } 128 | Err(AssetIoError::NotFound(path.to_path_buf())) 129 | } 130 | 131 | fn watch_path_for_changes( 132 | &self, 133 | _to_watch: &Path, 134 | _to_reload: Option, 135 | ) -> Result<(), AssetIoError> { 136 | Ok(()) 137 | } 138 | 139 | fn watch_for_changes(&self) -> Result<(), AssetIoError> { 140 | Ok(()) 141 | } 142 | 143 | fn get_metadata(&self, path: &Path) -> Result { 144 | info!("get_metadata: {:?}", path); 145 | if let Some(lock) = self.parent_dir_to_path_info.clone() { 146 | let mappings = lock.read().unwrap(); 147 | let path_str = normalize_path(path); 148 | if mappings.contains_key(&path_str) { 149 | Ok(bevy::asset::Metadata::new(bevy::asset::FileType::Directory)) 150 | } else { 151 | for v in mappings.values() { 152 | for info in v { 153 | if info.path() == path { 154 | return Ok(bevy::asset::Metadata::new(bevy::asset::FileType::File)); 155 | } 156 | } 157 | } 158 | Err(AssetIoError::NotFound(path.to_path_buf())) 159 | } 160 | } else { 161 | Err(AssetIoError::NotFound(path.to_path_buf())) 162 | } 163 | } 164 | } 165 | 166 | fn map_error(err: anyhow::Error) -> AssetIoError { 167 | AssetIoError::Io(std::io::Error::new( 168 | std::io::ErrorKind::Other, 169 | format!("{}", err), 170 | )) 171 | } 172 | 173 | fn normalize_path(path: &Path) -> String { 174 | path.to_str().unwrap_or("").replace('\\', "/") 175 | } 176 | -------------------------------------------------------------------------------- /src/plugin/bundled_asset_plugin.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | app::{App, Plugin}, 3 | asset::AssetServer, 4 | }; 5 | 6 | use super::BundledAssetIo; 7 | use crate::AssetBundlingOptions; 8 | 9 | #[derive(Default)] 10 | pub struct BundledAssetIoPlugin { 11 | options: AssetBundlingOptions, 12 | } 13 | 14 | impl From for BundledAssetIoPlugin { 15 | fn from(options: AssetBundlingOptions) -> Self { 16 | Self { options } 17 | } 18 | } 19 | 20 | impl Plugin for BundledAssetIoPlugin { 21 | fn build(&self, app: &mut App) { 22 | cfg_if::cfg_if! { 23 | if #[cfg(debug_assertions)]{ 24 | if !self.options.enabled_on_debug_build { 25 | warn!("disabled on debug build"); 26 | return; 27 | } 28 | } 29 | } 30 | 31 | let mut io = BundledAssetIo::from(self.options.clone()); 32 | match io.ensure_loaded() { 33 | Err(err) => { 34 | error!("Fail to load bundled asset: {:?}", err); 35 | } 36 | _ => { 37 | app.insert_resource(AssetServer::new(io)); 38 | } 39 | } 40 | } 41 | 42 | fn name(&self) -> &str { 43 | std::any::type_name::() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | mod bundled_asset_io; 2 | pub use bundled_asset_io::BundledAssetIo; 3 | 4 | mod bundled_asset_plugin; 5 | pub use bundled_asset_plugin::BundledAssetIoPlugin; 6 | 7 | mod path_info; 8 | -------------------------------------------------------------------------------- /src/plugin/path_info.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub(crate) struct ArchivePathInfo { 4 | path: PathBuf, 5 | } 6 | 7 | impl ArchivePathInfo { 8 | pub fn new(path: PathBuf) -> Self { 9 | Self { path } 10 | } 11 | 12 | pub fn path(&self) -> PathBuf { 13 | self.path.clone() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/bundler.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use super::common::prelude::*; 6 | 7 | #[test] 8 | fn invalid_asset_folder() -> anyhow::Result<()> { 9 | let options = create_default_options_with_random_bundle_name(); 10 | anyhow::ensure!(AssetBundler::from(options) 11 | .with_asset_folder("i/am/an/invalid/path") 12 | .build() 13 | .is_err()); 14 | Ok(()) 15 | } 16 | 17 | #[cfg(feature = "encryption")] 18 | #[test] 19 | fn no_encryption_key() -> anyhow::Result<()> { 20 | let mut options = create_default_options_with_random_bundle_name(); 21 | options.encryption_on = true; 22 | anyhow::ensure!(AssetBundler::from(options) 23 | .with_asset_folder(ASSET_PATH) 24 | .build() 25 | .is_err()); 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | #[cfg(test)] 3 | pub mod prelude { 4 | pub use bevy_assets_bundler::*; 5 | use rand::{prelude::*, rngs::OsRng}; 6 | 7 | pub const ASSET_PATH: &str = "example/assets"; 8 | 9 | pub fn create_default_options_with_random_bundle_name() -> AssetBundlingOptions { 10 | AssetBundlingOptions { 11 | asset_bundle_name: format!("{}.bin", uuid::Uuid::new_v4()), 12 | ..Default::default() 13 | } 14 | } 15 | 16 | pub fn create_random_key() -> [u8; 16] { 17 | let mut key = [0; 16]; 18 | OsRng.fill_bytes(&mut key); 19 | key 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/e2e.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use std::{ 6 | fs, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use super::common::prelude::*; 11 | 12 | #[test] 13 | fn e2e_all_default() { 14 | e2e_default_inner().unwrap(); 15 | } 16 | 17 | #[test] 18 | fn e2e_encryption_off_filename_encoding_on() { 19 | e2e_inner(false, true).unwrap(); 20 | } 21 | 22 | #[test] 23 | fn e2e_encryption_off_filename_encoding_off() { 24 | e2e_inner(false, false).unwrap(); 25 | } 26 | 27 | #[cfg(feature = "encryption")] 28 | #[test] 29 | fn e2e_encryption_on_filename_encoding_on() { 30 | e2e_inner(true, true).unwrap(); 31 | } 32 | 33 | #[cfg(feature = "encryption")] 34 | #[test] 35 | fn e2e_encryption_on_filename_encoding_off() { 36 | e2e_inner(true, false).unwrap(); 37 | } 38 | 39 | fn e2e_inner(enable_encryption: bool, encode_file_names: bool) -> anyhow::Result<()> { 40 | let mut options = create_default_options_with_random_bundle_name(); 41 | options.enabled_on_debug_build = true; 42 | options.encode_file_names = encode_file_names; 43 | if enable_encryption { 44 | #[cfg(feature = "encryption")] 45 | options.set_encryption_key(create_random_key()); 46 | } 47 | 48 | // build bundle 49 | let mut bundler = AssetBundler::from(options.clone()); 50 | bundler.asset_folder = ASSET_PATH.into(); 51 | bundler.build()?; 52 | 53 | // load bundle 54 | let mut asset_io = BundledAssetIo::from(options.clone()); 55 | verify_asset_io(&mut asset_io)?; 56 | Ok(()) 57 | } 58 | 59 | #[test] 60 | fn relative_bundle_path() -> anyhow::Result<()> { 61 | // build bundle 62 | let mut options = create_default_options_with_random_bundle_name(); 63 | options.asset_bundle_name = format!("dir/{}", options.asset_bundle_name); 64 | AssetBundler::from(options) 65 | .with_asset_folder(ASSET_PATH) 66 | .build()?; 67 | 68 | // load bundle 69 | let mut asset_io = BundledAssetIo::default(); 70 | verify_asset_io(&mut asset_io)?; 71 | Ok(()) 72 | } 73 | 74 | fn e2e_default_inner() -> anyhow::Result<()> { 75 | // build bundle 76 | AssetBundler::default() 77 | .with_asset_folder(ASSET_PATH) 78 | .build()?; 79 | 80 | // load bundle 81 | let mut asset_io = BundledAssetIo::default(); 82 | verify_asset_io(&mut asset_io)?; 83 | Ok(()) 84 | } 85 | 86 | fn verify_asset_io(asset_io: &mut BundledAssetIo) -> anyhow::Result<()> { 87 | asset_io.ensure_loaded()?; 88 | 89 | asset_io.watch_for_changes()?; 90 | asset_io.watch_path_for_changes(Path::new("any"), None)?; 91 | 92 | // Valid directories 93 | for dir in ["fonts", "nonascii/图", "nonascii\\图"] { 94 | anyhow::ensure!( 95 | asset_io.get_metadata(Path::new(dir))?.file_type() 96 | == bevy::asset::FileType::Directory 97 | ); 98 | let mut n = 0; 99 | for _ in asset_io.read_directory(Path::new(dir))? { 100 | n += 1; 101 | } 102 | anyhow::ensure!(n > 0); 103 | } 104 | 105 | // Invalid directories 106 | for dir in ["dummy", "fonts/dummy", "fonts\\dummy"] { 107 | anyhow::ensure!(asset_io.get_metadata(Path::new(dir)).is_err()); 108 | anyhow::ensure!(asset_io.read_directory(Path::new("dummy")).is_err()); 109 | } 110 | 111 | // Valid assets 112 | for asset_path in [ 113 | "branding/bevy_logo_dark_big.png", 114 | "fonts/FiraSans-Bold.ttf", 115 | "nonascii/图/图.png", 116 | ] { 117 | anyhow::ensure!( 118 | asset_io.get_metadata(Path::new(asset_path))?.file_type() 119 | == bevy::asset::FileType::File 120 | ); 121 | let future = asset_io.load_path(Path::new(asset_path)); 122 | anyhow::ensure!(futures_lite::future::block_on(async { 123 | match future.await { 124 | Ok(v) => { 125 | anyhow::ensure!(v.len() > 0); 126 | let mut file_path = PathBuf::from(ASSET_PATH); 127 | file_path.push(asset_path); 128 | let file_data = fs::read(file_path)?; 129 | anyhow::ensure!(v.len() == file_data.len()); 130 | anyhow::ensure!(v == file_data); 131 | } 132 | _ => { 133 | anyhow::ensure!(false); 134 | } 135 | }; 136 | Ok(()) 137 | }) 138 | .is_ok()); 139 | } 140 | 141 | // Valid assets windows path seperator 142 | for asset_path in [ 143 | "branding\\bevy_logo_dark_big.png", 144 | "fonts\\FiraSans-Bold.ttf", 145 | "nonascii\\图\\图.png", 146 | ] { 147 | let future = asset_io.load_path(Path::new(asset_path)); 148 | anyhow::ensure!(futures_lite::future::block_on(async { 149 | match future.await { 150 | Ok(v) => { 151 | anyhow::ensure!(v.len() > 0); 152 | let mut file_path = PathBuf::from(ASSET_PATH); 153 | file_path.push(asset_path.replace('\\', "/")); 154 | let file_data = fs::read(file_path)?; 155 | anyhow::ensure!(v.len() == file_data.len()); 156 | anyhow::ensure!(v == file_data); 157 | } 158 | _ => { 159 | anyhow::ensure!(false); 160 | } 161 | }; 162 | Ok(()) 163 | }) 164 | .is_ok()); 165 | } 166 | 167 | // Invalid assets 168 | for asset_path in ["branding/dummy.png", "dummy.svg"] { 169 | let future = asset_io.load_path(Path::new(asset_path)); 170 | anyhow::ensure!(futures_lite::future::block_on(async { 171 | anyhow::ensure!(future.await.is_err()); 172 | Ok(()) 173 | }) 174 | .is_ok()); 175 | } 176 | Ok(()) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/plugin.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use super::common::prelude::*; 6 | 7 | #[test] 8 | fn bundle_not_found() -> anyhow::Result<()> { 9 | let options = create_default_options_with_random_bundle_name(); 10 | let mut asset_io = BundledAssetIo::from(options); 11 | anyhow::ensure!(asset_io.ensure_loaded().is_err()); 12 | Ok(()) 13 | } 14 | 15 | #[cfg(feature = "encryption")] 16 | #[test] 17 | // #[should_panic] 18 | fn encryption_key_unmatched() -> anyhow::Result<()> { 19 | use std::path::Path; 20 | 21 | let mut options = create_default_options_with_random_bundle_name(); 22 | options.set_encryption_key(create_random_key()); 23 | AssetBundler::from(options.clone()) 24 | .with_asset_folder(ASSET_PATH) 25 | .build()?; 26 | 27 | // Reset key 28 | options.set_encryption_key(create_random_key()); 29 | let mut asset_io = BundledAssetIo::from(options.clone()); 30 | asset_io.ensure_loaded()?; 31 | let future = asset_io.load_path(Path::new("branding/bevy_logo_dark_big.png")); 32 | futures_lite::future::block_on(async { 33 | anyhow::ensure!(future.await.is_err()); 34 | Ok(()) 35 | }) 36 | } 37 | 38 | #[cfg(feature = "encryption")] 39 | #[test] 40 | #[should_panic] 41 | fn encryption_key_unmatched_filename_encoded() { 42 | let mut options = create_default_options_with_random_bundle_name(); 43 | options.encode_file_names = true; 44 | options.set_encryption_key(create_random_key()); 45 | AssetBundler::from(options.clone()) 46 | .with_asset_folder(ASSET_PATH) 47 | .build() 48 | .unwrap(); 49 | 50 | // Reset key 51 | options.set_encryption_key(create_random_key()); 52 | let mut asset_io = BundledAssetIo::from(options.clone()); 53 | asset_io.ensure_loaded().unwrap(); 54 | } 55 | } 56 | --------------------------------------------------------------------------------