├── tests ├── data │ └── README.md ├── to_base64_writer.rs ├── from_base64_writer.rs ├── to_base64_reader.rs └── from_base64_reader.rs ├── .github ├── dependabot.yml └── workflows │ ├── ci-version.yml │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── rustfmt.toml ├── .gitignore ├── src ├── to_base64_writer.rs ├── from_base64_writer.rs ├── lib.rs ├── to_base64_reader.rs └── from_base64_reader.rs └── README.md /tests/data/README.md: -------------------------------------------------------------------------------- 1 | This folder is for storing the output from test cases. It should be empty, and the folder itself needs to be tracked in Git. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "base64-stream" 3 | version = "4.0.1" 4 | authors = ["Magic Len "] 5 | edition = "2021" 6 | rust-version = "1.68" 7 | repository = "https://github.com/magiclen/base64-stream" 8 | homepage = "https://magiclen.org/base64-stream" 9 | keywords = ["base64", "stream", "file", "reader", "writer"] 10 | categories = ["encoding"] 11 | description = "To encode/decode large data with the standard Base64 encoding." 12 | license = "MIT" 13 | include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE"] 14 | 15 | [dependencies] 16 | base64 = "0.22" 17 | generic-array = "1" 18 | 19 | [dependencies.educe] 20 | version = "0.5" 21 | features = ["Debug"] 22 | default-features = false -------------------------------------------------------------------------------- /tests/to_base64_writer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | path::Path, 5 | }; 6 | 7 | use base64_stream::ToBase64Writer; 8 | 9 | const DATA_FOLDER: &str = "data"; 10 | const ENCODE_OUTPUT: &str = "encode_output.txt"; 11 | 12 | #[test] 13 | fn encode_write() { 14 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".as_ref(); 15 | 16 | let file_path = Path::new("tests").join(DATA_FOLDER).join(ENCODE_OUTPUT); 17 | 18 | let base64 = File::create(file_path.as_path()).unwrap(); 19 | 20 | let mut writer = ToBase64Writer::new(base64); 21 | 22 | writer.write_all(test_data).unwrap(); 23 | 24 | writer.flush().unwrap(); // the flush method is only used when the full plain data has been written 25 | 26 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", fs::read_to_string(file_path).unwrap()); 27 | } 28 | -------------------------------------------------------------------------------- /tests/from_base64_writer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | path::Path, 5 | }; 6 | 7 | use base64_stream::FromBase64Writer; 8 | 9 | const DATA_FOLDER: &str = "data"; 10 | const DECODE_OUTPUT: &str = "decode_output.txt"; 11 | 12 | #[test] 13 | fn decode_write() { 14 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".as_ref(); 15 | 16 | let file_path = Path::new("tests").join(DATA_FOLDER).join(DECODE_OUTPUT); 17 | 18 | let test_data = File::create(file_path.as_path()).unwrap(); 19 | 20 | let mut writer = FromBase64Writer::new(test_data); 21 | 22 | writer.write_all(base64).unwrap(); 23 | 24 | writer.flush().unwrap(); // the flush method is only used when the full base64 data has been written 25 | 26 | assert_eq!( 27 | "Hi there, this is a simple sentence used for testing this crate. I hope all cases are \ 28 | correct.", 29 | fs::read_to_string(file_path).unwrap() 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 magiclen.org (Ron Li) 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 | -------------------------------------------------------------------------------- /.github/workflows/ci-version.yml: -------------------------------------------------------------------------------- 1 | name: CI-version 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | toolchain: 21 | - stable 22 | - nightly 23 | features: 24 | - 25 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v6 29 | - uses: actions-rust-lang/setup-rust-toolchain@v1 30 | with: 31 | toolchain: ${{ matrix.toolchain }} 32 | - run: cargo test --release ${{ matrix.features }} 33 | - run: cargo doc --release ${{ matrix.features }} 34 | 35 | MSRV: 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | os: 40 | - ubuntu-latest 41 | - macos-latest 42 | - windows-latest 43 | toolchain: 44 | - 1.68 45 | features: 46 | - 47 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - uses: actions/checkout@v6 51 | - uses: actions-rust-lang/setup-rust-toolchain@v1 52 | with: 53 | toolchain: ${{ matrix.toolchain }} 54 | - run: cargo test --release --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /tests/to_base64_reader.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read}; 2 | 3 | use base64_stream::ToBase64Reader; 4 | 5 | #[test] 6 | fn encode_exact() { 7 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 8 | 9 | let mut reader = ToBase64Reader::new(Cursor::new(test_data)); 10 | 11 | let mut base64 = [0u8; 128]; 12 | 13 | reader.read_exact(&mut base64).unwrap(); 14 | 15 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".as_bytes(), base64.as_ref()); 16 | } 17 | 18 | #[test] 19 | fn encode_read_to_end() { 20 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 21 | 22 | let mut reader = ToBase64Reader::new(Cursor::new(test_data)); 23 | 24 | let mut base64 = Vec::new(); 25 | 26 | reader.read_to_end(&mut base64).unwrap(); 27 | 28 | assert_eq!(b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(), base64); 29 | } 30 | 31 | #[test] 32 | fn encode_read_to_string() { 33 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 34 | 35 | let mut reader = ToBase64Reader::new(Cursor::new(test_data)); 36 | 37 | let mut base64_string = String::new(); 38 | 39 | reader.read_to_string(&mut base64_string).unwrap(); 40 | 41 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", base64_string); 42 | } 43 | -------------------------------------------------------------------------------- /tests/from_base64_reader.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read}; 2 | 3 | use base64_stream::FromBase64Reader; 4 | 5 | #[test] 6 | fn decode_exact() { 7 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(); 8 | 9 | let mut reader = FromBase64Reader::new(Cursor::new(base64)); 10 | 11 | let mut test_data = [0u8; 94]; 12 | 13 | reader.read_exact(&mut test_data).unwrap(); 14 | 15 | assert_eq!( 16 | "Hi there, this is a simple sentence used for testing this crate. I hope all cases are \ 17 | correct." 18 | .as_bytes(), 19 | test_data.as_ref() 20 | ); 21 | } 22 | 23 | #[test] 24 | fn decode_to_end() { 25 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(); 26 | 27 | let mut reader = FromBase64Reader::new(Cursor::new(base64)); 28 | 29 | let mut test_data = Vec::new(); 30 | 31 | reader.read_to_end(&mut test_data).unwrap(); 32 | 33 | assert_eq!(b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(), test_data); 34 | } 35 | 36 | #[test] 37 | fn decode_to_string() { 38 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(); 39 | 40 | let mut reader = FromBase64Reader::new(Cursor::new(base64)); 41 | 42 | let mut test_data = String::new(); 43 | 44 | reader.read_to_string(&mut test_data).unwrap(); 45 | 46 | assert_eq!( 47 | "Hi there, this is a simple sentence used for testing this crate. I hope all cases are \ 48 | correct.", 49 | test_data 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | rustfmt: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rustfmt 17 | - uses: actions-rust-lang/rustfmt@v1 18 | 19 | clippy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v6 23 | - uses: actions-rust-lang/setup-rust-toolchain@v1 24 | with: 25 | components: clippy 26 | - run: cargo clippy --all-targets --all-features -- -D warnings 27 | 28 | tests: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: 33 | - ubuntu-latest 34 | - macos-latest 35 | - windows-latest 36 | toolchain: 37 | - stable 38 | - nightly 39 | features: 40 | - 41 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v6 45 | - uses: actions-rust-lang/setup-rust-toolchain@v1 46 | with: 47 | toolchain: ${{ matrix.toolchain }} 48 | - run: cargo test ${{ matrix.features }} 49 | - run: cargo doc ${{ matrix.features }} 50 | 51 | MSRV: 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | os: 56 | - ubuntu-latest 57 | - macos-latest 58 | - windows-latest 59 | toolchain: 60 | - 1.68 61 | features: 62 | - 63 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 64 | runs-on: ${{ matrix.os }} 65 | steps: 66 | - uses: actions/checkout@v6 67 | - uses: actions-rust-lang/setup-rust-toolchain@v1 68 | with: 69 | toolchain: ${{ matrix.toolchain }} 70 | - run: cargo test --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # array_width = 60 2 | # attr_fn_like_width = 70 3 | binop_separator = "Front" 4 | blank_lines_lower_bound = 0 5 | blank_lines_upper_bound = 1 6 | brace_style = "PreferSameLine" 7 | # chain_width = 60 8 | color = "Auto" 9 | # comment_width = 100 10 | condense_wildcard_suffixes = true 11 | control_brace_style = "AlwaysSameLine" 12 | empty_item_single_line = true 13 | enum_discrim_align_threshold = 80 14 | error_on_line_overflow = false 15 | error_on_unformatted = false 16 | # fn_call_width = 60 17 | fn_params_layout = "Tall" 18 | fn_single_line = false 19 | force_explicit_abi = true 20 | force_multiline_blocks = false 21 | format_code_in_doc_comments = true 22 | doc_comment_code_block_width = 80 23 | format_generated_files = true 24 | format_macro_matchers = true 25 | format_macro_bodies = true 26 | skip_macro_invocations = [] 27 | format_strings = true 28 | hard_tabs = false 29 | hex_literal_case = "Upper" 30 | imports_indent = "Block" 31 | imports_layout = "Mixed" 32 | indent_style = "Block" 33 | inline_attribute_width = 0 34 | match_arm_blocks = true 35 | match_arm_leading_pipes = "Never" 36 | match_block_trailing_comma = true 37 | max_width = 100 38 | merge_derives = true 39 | imports_granularity = "Crate" 40 | newline_style = "Unix" 41 | normalize_comments = false 42 | normalize_doc_attributes = true 43 | overflow_delimited_expr = true 44 | remove_nested_parens = true 45 | reorder_impl_items = true 46 | reorder_imports = true 47 | group_imports = "StdExternalCrate" 48 | reorder_modules = true 49 | short_array_element_width_threshold = 10 50 | # single_line_if_else_max_width = 50 51 | space_after_colon = true 52 | space_before_colon = false 53 | spaces_around_ranges = false 54 | struct_field_align_threshold = 80 55 | struct_lit_single_line = false 56 | # struct_lit_width = 18 57 | # struct_variant_width = 35 58 | tab_spaces = 4 59 | trailing_comma = "Vertical" 60 | trailing_semicolon = true 61 | type_punctuation_density = "Wide" 62 | use_field_init_shorthand = true 63 | use_small_heuristics = "Max" 64 | use_try_shorthand = true 65 | where_single_line = false 66 | wrap_comments = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*_output.* 2 | 3 | ### Intellij+all ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # AWS User-specific 15 | .idea/**/aws.xml 16 | 17 | # Generated files 18 | .idea/**/contentModel.xml 19 | 20 | # Sensitive or high-churn files 21 | .idea/**/dataSources/ 22 | .idea/**/dataSources.ids 23 | .idea/**/dataSources.local.xml 24 | .idea/**/sqlDataSources.xml 25 | .idea/**/dynamic.xml 26 | .idea/**/uiDesigner.xml 27 | .idea/**/dbnavigator.xml 28 | 29 | # Gradle 30 | .idea/**/gradle.xml 31 | .idea/**/libraries 32 | 33 | # Gradle and Maven with auto-import 34 | # When using Gradle or Maven with auto-import, you should exclude module files, 35 | # since they will be recreated, and may cause churn. Uncomment if using 36 | # auto-import. 37 | # .idea/artifacts 38 | # .idea/compiler.xml 39 | # .idea/jarRepositories.xml 40 | # .idea/modules.xml 41 | # .idea/*.iml 42 | # .idea/modules 43 | # *.iml 44 | # *.ipr 45 | 46 | # CMake 47 | cmake-build-*/ 48 | 49 | # Mongo Explorer plugin 50 | .idea/**/mongoSettings.xml 51 | 52 | # File-based project format 53 | *.iws 54 | 55 | # IntelliJ 56 | out/ 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Cursive Clojure plugin 65 | .idea/replstate.xml 66 | 67 | # SonarLint plugin 68 | .idea/sonarlint/ 69 | 70 | # Crashlytics plugin (for Android Studio and IntelliJ) 71 | com_crashlytics_export_strings.xml 72 | crashlytics.properties 73 | crashlytics-build.properties 74 | fabric.properties 75 | 76 | # Editor-based Rest Client 77 | .idea/httpRequests 78 | 79 | # Android studio 3.1+ serialized cache file 80 | .idea/caches/build_file_checksums.ser 81 | 82 | ### Intellij+all Patch ### 83 | # Ignore everything but code style settings and run configurations 84 | # that are supposed to be shared within teams. 85 | 86 | .idea/* 87 | 88 | !.idea/codeStyles 89 | !.idea/runConfigurations 90 | 91 | ### Rust ### 92 | # Generated by Cargo 93 | # will have compiled files and executables 94 | debug/ 95 | target/ 96 | 97 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 98 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 99 | Cargo.lock 100 | 101 | # These are backup files generated by rustfmt 102 | **/*.rs.bk 103 | 104 | # MSVC Windows builds of rustc generate these, which store debugging information 105 | *.pdb 106 | 107 | ### Vim ### 108 | # Swap 109 | [._]*.s[a-v][a-z] 110 | !*.svg # comment out if you don't need vector files 111 | [._]*.sw[a-p] 112 | [._]s[a-rt-v][a-z] 113 | [._]ss[a-gi-z] 114 | [._]sw[a-p] 115 | 116 | # Session 117 | Session.vim 118 | Sessionx.vim 119 | 120 | # Temporary 121 | .netrwhist 122 | *~ 123 | # Auto-generated tag files 124 | tags 125 | # Persistent undo 126 | [._]*.un~ 127 | 128 | ### VisualStudioCode ### 129 | .vscode/* 130 | !.vscode/settings.json 131 | !.vscode/tasks.json 132 | !.vscode/launch.json 133 | !.vscode/extensions.json 134 | !.vscode/*.code-snippets 135 | 136 | # Local History for Visual Studio Code 137 | .history/ 138 | 139 | # Built Visual Studio Code Extensions 140 | *.vsix 141 | 142 | ### VisualStudioCode Patch ### 143 | # Ignore all local history of files 144 | .history 145 | .ionide -------------------------------------------------------------------------------- /src/to_base64_writer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Write}, 3 | ptr::copy_nonoverlapping, 4 | }; 5 | 6 | use base64::{ 7 | engine::{general_purpose::STANDARD, GeneralPurpose}, 8 | Engine, 9 | }; 10 | use generic_array::{ 11 | typenum::{IsGreaterOrEqual, True, U4, U4096}, 12 | ArrayLength, GenericArray, 13 | }; 14 | 15 | /// Write base64 data and encode them to plain data. 16 | #[derive(Educe)] 17 | #[educe(Debug)] 18 | pub struct ToBase64Writer = U4096> { 19 | #[educe(Debug(ignore))] 20 | inner: W, 21 | buf: [u8; 3], 22 | buf_length: usize, 23 | temp: GenericArray, 24 | #[educe(Debug(ignore))] 25 | engine: &'static GeneralPurpose, 26 | } 27 | 28 | impl ToBase64Writer { 29 | #[inline] 30 | pub fn new(writer: W) -> ToBase64Writer { 31 | Self::new2(writer) 32 | } 33 | } 34 | 35 | impl> ToBase64Writer { 36 | #[inline] 37 | pub fn new2(writer: W) -> ToBase64Writer { 38 | ToBase64Writer { 39 | inner: writer, 40 | buf: [0; 3], 41 | buf_length: 0, 42 | temp: GenericArray::default(), 43 | engine: &STANDARD, 44 | } 45 | } 46 | } 47 | 48 | impl> ToBase64Writer { 49 | fn drain_block(&mut self) -> Result<(), io::Error> { 50 | debug_assert!(self.buf_length > 0); 51 | 52 | let encode_length = 53 | self.engine.encode_slice(&self.buf[..self.buf_length], &mut self.temp).unwrap(); 54 | 55 | self.inner.write_all(&self.temp[..encode_length])?; 56 | 57 | self.buf_length = 0; 58 | 59 | Ok(()) 60 | } 61 | } 62 | 63 | impl> Write 64 | for ToBase64Writer 65 | { 66 | fn write(&mut self, mut buf: &[u8]) -> Result { 67 | let original_buf_length = buf.len(); 68 | 69 | if self.buf_length == 0 { 70 | while buf.len() >= 3 { 71 | let max_available_buf_length = 72 | (buf.len() - (buf.len() % 3)).min((N::USIZE >> 2) * 3); // (N::USIZE / 4) * 3 73 | 74 | let encode_length = self 75 | .engine 76 | .encode_slice(&buf[..max_available_buf_length], &mut self.temp) 77 | .unwrap(); 78 | 79 | buf = &buf[max_available_buf_length..]; 80 | 81 | self.inner.write_all(&self.temp[..encode_length])?; 82 | } 83 | 84 | let buf_length = buf.len(); 85 | 86 | if buf_length > 0 { 87 | unsafe { 88 | copy_nonoverlapping(buf.as_ptr(), self.buf.as_mut_ptr(), buf_length); 89 | } 90 | 91 | self.buf_length = buf_length; 92 | } 93 | } else { 94 | debug_assert!(self.buf_length < 3); 95 | 96 | let r = 3 - self.buf_length; 97 | 98 | let buf_length = buf.len(); 99 | 100 | let drain_length = r.min(buf_length); 101 | 102 | unsafe { 103 | copy_nonoverlapping( 104 | buf.as_ptr(), 105 | self.buf.as_mut_ptr().add(self.buf_length), 106 | drain_length, 107 | ); 108 | } 109 | 110 | buf = &buf[drain_length..]; 111 | 112 | self.buf_length += drain_length; 113 | 114 | if self.buf_length == 3 { 115 | self.drain_block()?; 116 | 117 | if buf_length > r { 118 | self.write_all(buf)?; 119 | } 120 | } 121 | } 122 | 123 | Ok(original_buf_length) 124 | } 125 | 126 | fn flush(&mut self) -> Result<(), io::Error> { 127 | if self.buf_length > 0 { 128 | self.drain_block()?; 129 | } 130 | 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl From for ToBase64Writer { 136 | #[inline] 137 | fn from(reader: W) -> Self { 138 | ToBase64Writer::new(reader) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/from_base64_writer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, ErrorKind, Write}, 3 | ptr::copy_nonoverlapping, 4 | }; 5 | 6 | use base64::{ 7 | engine::{general_purpose::STANDARD, GeneralPurpose}, 8 | Engine, 9 | }; 10 | use generic_array::{ 11 | typenum::{IsGreaterOrEqual, True, U4, U4096}, 12 | ArrayLength, GenericArray, 13 | }; 14 | 15 | /// Write base64 data and decode them to plain data. 16 | #[derive(Educe)] 17 | #[educe(Debug)] 18 | pub struct FromBase64Writer = U4096> 19 | { 20 | #[educe(Debug(ignore))] 21 | inner: W, 22 | buf: [u8; 4], 23 | buf_length: usize, 24 | temp: GenericArray, 25 | #[educe(Debug(ignore))] 26 | engine: &'static GeneralPurpose, 27 | } 28 | 29 | impl FromBase64Writer { 30 | #[inline] 31 | pub fn new(writer: W) -> FromBase64Writer { 32 | Self::new2(writer) 33 | } 34 | } 35 | 36 | impl> FromBase64Writer { 37 | #[inline] 38 | pub fn new2(writer: W) -> FromBase64Writer { 39 | FromBase64Writer { 40 | inner: writer, 41 | buf: [0; 4], 42 | buf_length: 0, 43 | temp: GenericArray::default(), 44 | engine: &STANDARD, 45 | } 46 | } 47 | } 48 | 49 | impl> FromBase64Writer { 50 | fn drain_block(&mut self) -> Result<(), io::Error> { 51 | debug_assert!(self.buf_length > 0); 52 | 53 | let decode_length = self 54 | .engine 55 | .decode_slice(&self.buf[..self.buf_length], &mut self.temp) 56 | .map_err(|err| io::Error::new(ErrorKind::Other, err))?; 57 | 58 | self.inner.write_all(&self.temp[..decode_length])?; 59 | 60 | self.buf_length = 0; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | impl> Write 67 | for FromBase64Writer 68 | { 69 | fn write(&mut self, mut buf: &[u8]) -> Result { 70 | let original_buf_length = buf.len(); 71 | 72 | if self.buf_length == 0 { 73 | while buf.len() >= 4 { 74 | let max_available_buf_length = (buf.len() & !0b11).min((N::USIZE / 3) << 2); // (N::USIZE / 3) * 4 75 | 76 | let decode_length = self 77 | .engine 78 | .decode_slice(&buf[..max_available_buf_length], &mut self.temp) 79 | .map_err(|err| io::Error::new(ErrorKind::Other, err))?; 80 | 81 | buf = &buf[max_available_buf_length..]; 82 | 83 | self.inner.write_all(&self.temp[..decode_length])?; 84 | } 85 | 86 | let buf_length = buf.len(); 87 | 88 | if buf_length > 0 { 89 | unsafe { 90 | copy_nonoverlapping(buf.as_ptr(), self.buf.as_mut_ptr(), buf_length); 91 | } 92 | 93 | self.buf_length = buf_length; 94 | } 95 | } else { 96 | debug_assert!(self.buf_length < 4); 97 | 98 | let r = 4 - self.buf_length; 99 | 100 | let buf_length = buf.len(); 101 | 102 | let drain_length = r.min(buf_length); 103 | 104 | unsafe { 105 | copy_nonoverlapping( 106 | buf.as_ptr(), 107 | self.buf.as_mut_ptr().add(self.buf_length), 108 | drain_length, 109 | ); 110 | } 111 | 112 | buf = &buf[drain_length..]; 113 | 114 | self.buf_length += drain_length; 115 | 116 | if self.buf_length == 4 { 117 | self.drain_block()?; 118 | 119 | if buf_length > r { 120 | self.write_all(buf)?; 121 | } 122 | } 123 | } 124 | 125 | Ok(original_buf_length) 126 | } 127 | 128 | #[inline] 129 | fn flush(&mut self) -> Result<(), io::Error> { 130 | if self.buf_length > 0 { 131 | self.drain_block()?; 132 | } 133 | 134 | Ok(()) 135 | } 136 | } 137 | 138 | impl From for FromBase64Writer { 139 | #[inline] 140 | fn from(reader: W) -> Self { 141 | FromBase64Writer::new(reader) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Base64 Stream 2 | ==================== 3 | 4 | [![CI](https://github.com/magiclen/base64-stream/actions/workflows/ci.yml/badge.svg)](https://github.com/magiclen/base64-stream/actions/workflows/ci.yml) 5 | 6 | To encode/decode large data with the standard Base64 encoding. 7 | 8 | ## Examples 9 | 10 | ### Encode 11 | 12 | #### ToBase64Reader 13 | 14 | ```rust 15 | use std::io::{Cursor, Read}; 16 | 17 | use base64_stream::ToBase64Reader; 18 | 19 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 20 | 21 | let mut reader = ToBase64Reader::new(Cursor::new(test_data)); 22 | 23 | let mut base64 = String::new(); 24 | 25 | reader.read_to_string(&mut base64).unwrap(); 26 | 27 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", base64); 28 | ``` 29 | 30 | #### ToBase64Writer 31 | 32 | ```rust 33 | use std::fs::{self, File}; 34 | use std::io::Write; 35 | use std::path::Path; 36 | 37 | use base64_stream::ToBase64Writer; 38 | 39 | const DATA_FOLDER: &str = "data"; 40 | const ENCODE_OUTPUT: &str = "encode_output.txt"; 41 | 42 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".as_ref(); 43 | 44 | let file_path = Path::new("tests").join(DATA_FOLDER).join(ENCODE_OUTPUT); 45 | 46 | let base64 = File::create(file_path.as_path()).unwrap(); 47 | 48 | let mut writer = ToBase64Writer::new(base64); 49 | 50 | writer.write_all(test_data).unwrap(); 51 | 52 | writer.flush().unwrap(); // the flush method is only used when the full plain data has been written 53 | 54 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", fs::read_to_string(file_path).unwrap()); 55 | ``` 56 | 57 | ### Decode 58 | 59 | #### FromBase64Reader 60 | 61 | ```rust 62 | use std::io::Cursor; 63 | 64 | use std::io::Read; 65 | 66 | use base64_stream::FromBase64Reader; 67 | 68 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(); 69 | 70 | let mut reader = FromBase64Reader::new(Cursor::new(base64)); 71 | 72 | let mut test_data = String::new(); 73 | 74 | reader.read_to_string(&mut test_data).unwrap(); 75 | 76 | assert_eq!("Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.", test_data); 77 | ``` 78 | 79 | #### FromBase64Writer 80 | 81 | ```rust 82 | use std::fs::{self, File}; 83 | use std::io::Write; 84 | use std::path::Path; 85 | 86 | use base64_stream::FromBase64Writer; 87 | 88 | const DATA_FOLDER: &str = "data"; 89 | const DECODE_OUTPUT: &str = "decode_output.txt"; 90 | 91 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".as_ref(); 92 | 93 | let file_path = Path::new("tests").join(DATA_FOLDER).join(DECODE_OUTPUT); 94 | 95 | let test_data = File::create(file_path.as_path()).unwrap(); 96 | 97 | let mut writer = FromBase64Writer::new(test_data); 98 | 99 | writer.write_all(base64).unwrap(); 100 | 101 | writer.flush().unwrap(); // the flush method is only used when the full base64 data has been written 102 | 103 | assert_eq!("Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.", fs::read_to_string(file_path).unwrap()); 104 | ``` 105 | 106 | ## Change the Buffer Size 107 | 108 | The default buffer size is 4096 bytes. If you want to change that, you can use the `new2` associated function and define a length explicitly to create an instance of the above structs. 109 | 110 | For example, to change the buffer size to 256 bytes, 111 | 112 | ```rust 113 | use std::io::{Cursor, Read}; 114 | 115 | use base64_stream::ToBase64Reader; 116 | use base64_stream::generic_array::typenum::U256; 117 | 118 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 119 | 120 | let mut reader: ToBase64Reader<_, U256> = ToBase64Reader::new2(Cursor::new(test_data)); 121 | 122 | let mut base64 = String::new(); 123 | 124 | reader.read_to_string(&mut base64).unwrap(); 125 | 126 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", base64); 127 | ``` 128 | 129 | ## Crates.io 130 | 131 | https://crates.io/crates/base64-stream 132 | 133 | ## Documentation 134 | 135 | https://docs.rs/base64-stream 136 | 137 | ## License 138 | 139 | [MIT](LICENSE) -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Base64 Stream 3 | 4 | To encode/decode large data with the standard Base64 encoding. 5 | 6 | ## Examples 7 | 8 | ### Encode 9 | 10 | #### ToBase64Reader 11 | 12 | ```rust 13 | use std::io::{Cursor, Read}; 14 | 15 | use base64_stream::ToBase64Reader; 16 | 17 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 18 | 19 | let mut reader = ToBase64Reader::new(Cursor::new(test_data)); 20 | 21 | let mut base64 = String::new(); 22 | 23 | reader.read_to_string(&mut base64).unwrap(); 24 | 25 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", base64); 26 | ``` 27 | 28 | #### ToBase64Writer 29 | 30 | ```rust 31 | use std::fs::{self, File}; 32 | use std::io::Write; 33 | use std::path::Path; 34 | 35 | use base64_stream::ToBase64Writer; 36 | 37 | const DATA_FOLDER: &str = "data"; 38 | const ENCODE_OUTPUT: &str = "encode_output.txt"; 39 | 40 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".as_ref(); 41 | 42 | let file_path = Path::new("tests").join(DATA_FOLDER).join(ENCODE_OUTPUT); 43 | 44 | let base64 = File::create(file_path.as_path()).unwrap(); 45 | 46 | let mut writer = ToBase64Writer::new(base64); 47 | 48 | writer.write_all(test_data).unwrap(); 49 | 50 | writer.flush().unwrap(); // the flush method is only used when the full plain data has been written 51 | 52 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", fs::read_to_string(file_path).unwrap()); 53 | ``` 54 | 55 | ### Decode 56 | 57 | #### FromBase64Reader 58 | 59 | ```rust 60 | use std::io::Cursor; 61 | 62 | use std::io::Read; 63 | 64 | use base64_stream::FromBase64Reader; 65 | 66 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".to_vec(); 67 | 68 | let mut reader = FromBase64Reader::new(Cursor::new(base64)); 69 | 70 | let mut test_data = String::new(); 71 | 72 | reader.read_to_string(&mut test_data).unwrap(); 73 | 74 | assert_eq!("Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.", test_data); 75 | ``` 76 | 77 | #### FromBase64Writer 78 | 79 | ```rust 80 | use std::fs::{self, File}; 81 | use std::io::Write; 82 | use std::path::Path; 83 | 84 | use base64_stream::FromBase64Writer; 85 | 86 | const DATA_FOLDER: &str = "data"; 87 | const DECODE_OUTPUT: &str = "decode_output.txt"; 88 | 89 | let base64 = b"SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==".as_ref(); 90 | 91 | let file_path = Path::new("tests").join(DATA_FOLDER).join(DECODE_OUTPUT); 92 | 93 | let test_data = File::create(file_path.as_path()).unwrap(); 94 | 95 | let mut writer = FromBase64Writer::new(test_data); 96 | 97 | writer.write_all(base64).unwrap(); 98 | 99 | writer.flush().unwrap(); // the flush method is only used when the full base64 data has been written 100 | 101 | assert_eq!("Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.", fs::read_to_string(file_path).unwrap()); 102 | ``` 103 | 104 | ## Change the Buffer Size 105 | 106 | The default buffer size is 4096 bytes. If you want to change that, you can use the `new2` associated function and define a length explicitly to create an instance of the above structs. 107 | 108 | For example, to change the buffer size to 256 bytes, 109 | 110 | ```rust 111 | use std::io::{Cursor, Read}; 112 | 113 | use base64_stream::ToBase64Reader; 114 | use base64_stream::generic_array::typenum::U256; 115 | 116 | let test_data = b"Hi there, this is a simple sentence used for testing this crate. I hope all cases are correct.".to_vec(); 117 | 118 | let mut reader: ToBase64Reader<_, U256> = ToBase64Reader::new2(Cursor::new(test_data)); 119 | 120 | let mut base64 = String::new(); 121 | 122 | reader.read_to_string(&mut base64).unwrap(); 123 | 124 | assert_eq!("SGkgdGhlcmUsIHRoaXMgaXMgYSBzaW1wbGUgc2VudGVuY2UgdXNlZCBmb3IgdGVzdGluZyB0aGlzIGNyYXRlLiBJIGhvcGUgYWxsIGNhc2VzIGFyZSBjb3JyZWN0Lg==", base64); 125 | ``` 126 | */ 127 | 128 | pub extern crate base64; 129 | pub extern crate generic_array; 130 | 131 | #[macro_use] 132 | extern crate educe; 133 | 134 | mod from_base64_reader; 135 | mod from_base64_writer; 136 | mod to_base64_reader; 137 | mod to_base64_writer; 138 | 139 | pub use from_base64_reader::*; 140 | pub use from_base64_writer::*; 141 | pub use to_base64_reader::*; 142 | pub use to_base64_writer::*; 143 | -------------------------------------------------------------------------------- /src/to_base64_reader.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, ErrorKind, Read}, 3 | ptr::{copy, copy_nonoverlapping}, 4 | }; 5 | 6 | use base64::{ 7 | engine::{general_purpose::STANDARD, GeneralPurpose}, 8 | Engine, 9 | }; 10 | use generic_array::{ 11 | typenum::{IsGreaterOrEqual, True, U4, U4096}, 12 | ArrayLength, GenericArray, 13 | }; 14 | 15 | /// Read any data and encode them to base64 data. 16 | #[derive(Educe)] 17 | #[educe(Debug)] 18 | pub struct ToBase64Reader = U4096> { 19 | #[educe(Debug(ignore))] 20 | inner: R, 21 | buf: GenericArray, 22 | buf_length: usize, 23 | buf_offset: usize, 24 | temp: [u8; 3], 25 | temp_length: usize, 26 | #[educe(Debug(ignore))] 27 | engine: &'static GeneralPurpose, 28 | } 29 | 30 | impl ToBase64Reader { 31 | #[inline] 32 | pub fn new(reader: R) -> ToBase64Reader { 33 | Self::new2(reader) 34 | } 35 | } 36 | 37 | impl> ToBase64Reader { 38 | #[inline] 39 | pub fn new2(reader: R) -> ToBase64Reader { 40 | ToBase64Reader { 41 | inner: reader, 42 | buf: GenericArray::default(), 43 | buf_length: 0, 44 | buf_offset: 0, 45 | temp: [0; 3], 46 | temp_length: 0, 47 | engine: &STANDARD, 48 | } 49 | } 50 | } 51 | 52 | impl> ToBase64Reader { 53 | fn buf_left_shift(&mut self, distance: usize) { 54 | debug_assert!(self.buf_length >= distance); 55 | 56 | self.buf_offset += distance; 57 | 58 | if self.buf_offset >= N::USIZE - 4 { 59 | unsafe { 60 | copy( 61 | self.buf.as_ptr().add(self.buf_offset), 62 | self.buf.as_mut_ptr(), 63 | self.buf_length, 64 | ); 65 | } 66 | 67 | self.buf_offset = 0; 68 | } 69 | 70 | self.buf_length -= distance; 71 | } 72 | 73 | #[inline] 74 | fn drain_temp<'a>(&mut self, buf: &'a mut [u8]) -> &'a mut [u8] { 75 | debug_assert!(self.temp_length > 0); 76 | debug_assert!(!buf.is_empty()); 77 | 78 | let drain_length = buf.len().min(self.temp_length); 79 | 80 | unsafe { 81 | copy_nonoverlapping(self.temp.as_ptr(), buf.as_mut_ptr(), drain_length); 82 | } 83 | 84 | self.temp_length -= drain_length; 85 | 86 | unsafe { 87 | copy( 88 | self.temp.as_ptr().add(self.temp_length), 89 | self.temp.as_mut_ptr(), 90 | self.temp_length, 91 | ); 92 | } 93 | 94 | &mut buf[drain_length..] 95 | } 96 | 97 | #[inline] 98 | fn drain_block<'a>(&mut self, mut buf: &'a mut [u8]) -> &'a mut [u8] { 99 | debug_assert!(self.buf_length > 0); 100 | debug_assert!(self.temp_length == 0); 101 | debug_assert!(!buf.is_empty()); 102 | 103 | let drain_length = self.buf_length.min(3); 104 | 105 | let mut b = [0; 4]; 106 | 107 | let encode_length = self 108 | .engine 109 | .encode_slice(&self.buf[self.buf_offset..(self.buf_offset + drain_length)], &mut b) 110 | .unwrap(); 111 | 112 | self.buf_left_shift(drain_length); 113 | 114 | let buf_length = buf.len(); 115 | 116 | if buf_length >= encode_length { 117 | unsafe { 118 | copy_nonoverlapping(b.as_ptr(), buf.as_mut_ptr(), encode_length); 119 | } 120 | 121 | buf = &mut buf[encode_length..]; 122 | } else { 123 | unsafe { 124 | copy_nonoverlapping(b.as_ptr(), buf.as_mut_ptr(), buf_length); 125 | } 126 | 127 | buf = &mut buf[buf_length..]; 128 | 129 | self.temp_length = encode_length - buf_length; 130 | 131 | unsafe { 132 | copy_nonoverlapping( 133 | b.as_ptr().add(buf_length), 134 | self.temp.as_mut_ptr(), 135 | self.temp_length, 136 | ); 137 | } 138 | } 139 | 140 | buf 141 | } 142 | 143 | fn drain<'a>(&mut self, mut buf: &'a mut [u8]) -> &'a mut [u8] { 144 | if buf.is_empty() { 145 | return buf; 146 | } 147 | 148 | if self.temp_length > 0 { 149 | buf = self.drain_temp(buf); 150 | } 151 | 152 | debug_assert!(self.buf_length >= 3); 153 | 154 | let buf_length = buf.len(); 155 | 156 | if buf_length >= 4 { 157 | debug_assert!(self.temp_length == 0); 158 | 159 | let actual_max_read_size = (buf_length >> 2) * 3; // (buf_length / 4) * 3 160 | let max_available_self_buf_length = self.buf_length - (self.buf_length % 3); 161 | 162 | let drain_length = max_available_self_buf_length.min(actual_max_read_size); 163 | 164 | let encode_length = self 165 | .engine 166 | .encode_slice(&self.buf[self.buf_offset..(self.buf_offset + drain_length)], buf) 167 | .unwrap(); 168 | 169 | buf = &mut buf[encode_length..]; 170 | 171 | self.buf_left_shift(drain_length); 172 | } 173 | 174 | if !buf.is_empty() && self.buf_length >= 3 { 175 | self.drain_block(buf) 176 | } else { 177 | buf 178 | } 179 | } 180 | 181 | #[inline] 182 | fn drain_end<'a>(&mut self, mut buf: &'a mut [u8]) -> &'a mut [u8] { 183 | if buf.is_empty() { 184 | return buf; 185 | } 186 | 187 | if self.temp_length > 0 { 188 | buf = self.drain_temp(buf); 189 | } 190 | 191 | if !buf.is_empty() && self.buf_length > 0 { 192 | self.drain_block(buf) 193 | } else { 194 | buf 195 | } 196 | } 197 | } 198 | 199 | impl> Read for ToBase64Reader { 200 | fn read(&mut self, mut buf: &mut [u8]) -> Result { 201 | let original_buf_length = buf.len(); 202 | 203 | while self.buf_length < 3 { 204 | match self.inner.read(&mut self.buf[(self.buf_offset + self.buf_length)..]) { 205 | Ok(0) => { 206 | buf = self.drain_end(buf); 207 | 208 | return Ok(original_buf_length - buf.len()); 209 | }, 210 | Ok(c) => self.buf_length += c, 211 | Err(ref e) if e.kind() == ErrorKind::Interrupted => {}, 212 | Err(e) => return Err(e), 213 | } 214 | } 215 | 216 | buf = self.drain(buf); 217 | 218 | Ok(original_buf_length - buf.len()) 219 | } 220 | } 221 | 222 | impl From for ToBase64Reader { 223 | #[inline] 224 | fn from(reader: R) -> Self { 225 | ToBase64Reader::new(reader) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/from_base64_reader.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, ErrorKind, Read}, 3 | ptr::{copy, copy_nonoverlapping}, 4 | }; 5 | 6 | use base64::{ 7 | engine::{general_purpose::STANDARD, GeneralPurpose}, 8 | DecodeSliceError, Engine, 9 | }; 10 | use generic_array::{ 11 | typenum::{IsGreaterOrEqual, True, U4, U4096}, 12 | ArrayLength, GenericArray, 13 | }; 14 | 15 | /// Read base64 data and decode them to plain data. 16 | #[derive(Educe)] 17 | #[educe(Debug)] 18 | pub struct FromBase64Reader = U4096> { 19 | #[educe(Debug(ignore))] 20 | inner: R, 21 | buf: GenericArray, 22 | buf_length: usize, 23 | buf_offset: usize, 24 | temp: [u8; 2], 25 | temp_length: usize, 26 | #[educe(Debug(ignore))] 27 | engine: &'static GeneralPurpose, 28 | } 29 | 30 | impl FromBase64Reader { 31 | #[inline] 32 | pub fn new(reader: R) -> FromBase64Reader { 33 | Self::new2(reader) 34 | } 35 | } 36 | 37 | impl> FromBase64Reader { 38 | #[inline] 39 | pub fn new2(reader: R) -> FromBase64Reader { 40 | FromBase64Reader { 41 | inner: reader, 42 | buf: GenericArray::default(), 43 | buf_length: 0, 44 | buf_offset: 0, 45 | temp: [0; 2], 46 | temp_length: 0, 47 | engine: &STANDARD, 48 | } 49 | } 50 | } 51 | 52 | impl> FromBase64Reader { 53 | fn buf_left_shift(&mut self, distance: usize) { 54 | debug_assert!(self.buf_length >= distance); 55 | 56 | self.buf_offset += distance; 57 | 58 | if self.buf_offset >= N::USIZE - 4 { 59 | unsafe { 60 | copy( 61 | self.buf.as_ptr().add(self.buf_offset), 62 | self.buf.as_mut_ptr(), 63 | self.buf_length, 64 | ); 65 | } 66 | 67 | self.buf_offset = 0; 68 | } 69 | 70 | self.buf_length -= distance; 71 | } 72 | 73 | #[inline] 74 | fn drain_temp<'a>(&mut self, buf: &'a mut [u8]) -> &'a mut [u8] { 75 | debug_assert!(self.temp_length > 0); 76 | debug_assert!(!buf.is_empty()); 77 | 78 | let drain_length = buf.len().min(self.temp_length); 79 | 80 | unsafe { 81 | copy_nonoverlapping(self.temp.as_ptr(), buf.as_mut_ptr(), drain_length); 82 | } 83 | 84 | self.temp_length -= drain_length; 85 | 86 | unsafe { 87 | copy( 88 | self.temp.as_ptr().add(self.temp_length), 89 | self.temp.as_mut_ptr(), 90 | self.temp_length, 91 | ); 92 | } 93 | 94 | &mut buf[drain_length..] 95 | } 96 | 97 | #[inline] 98 | fn drain_block<'a>(&mut self, mut buf: &'a mut [u8]) -> Result<&'a mut [u8], DecodeSliceError> { 99 | debug_assert!(self.buf_length > 0); 100 | debug_assert!(self.temp_length == 0); 101 | debug_assert!(!buf.is_empty()); 102 | 103 | let drain_length = self.buf_length.min(4); 104 | 105 | let mut b = [0; 3]; 106 | 107 | let decode_length = self 108 | .engine 109 | .decode_slice(&self.buf[self.buf_offset..(self.buf_offset + drain_length)], &mut b)?; 110 | 111 | self.buf_left_shift(drain_length); 112 | 113 | let buf_length = buf.len(); 114 | 115 | if buf_length >= decode_length { 116 | unsafe { 117 | copy_nonoverlapping(b.as_ptr(), buf.as_mut_ptr(), decode_length); 118 | } 119 | 120 | buf = &mut buf[decode_length..]; 121 | } else { 122 | unsafe { 123 | copy_nonoverlapping(b.as_ptr(), buf.as_mut_ptr(), buf_length); 124 | } 125 | 126 | buf = &mut buf[buf_length..]; 127 | 128 | self.temp_length = decode_length - buf_length; 129 | 130 | unsafe { 131 | copy_nonoverlapping( 132 | b.as_ptr().add(buf_length), 133 | self.temp.as_mut_ptr(), 134 | self.temp_length, 135 | ); 136 | } 137 | } 138 | 139 | Ok(buf) 140 | } 141 | 142 | fn drain<'a>(&mut self, mut buf: &'a mut [u8]) -> Result<&'a mut [u8], DecodeSliceError> { 143 | if buf.is_empty() { 144 | return Ok(buf); 145 | } 146 | 147 | if self.temp_length > 0 { 148 | buf = self.drain_temp(buf); 149 | } 150 | 151 | debug_assert!(self.buf_length >= 4); 152 | 153 | let buf_length = buf.len(); 154 | 155 | if buf_length >= 3 { 156 | debug_assert!(self.temp_length == 0); 157 | 158 | let actual_max_read_size = (buf_length / 3) << 2; // (buf_length / 3) * 4 159 | let max_available_self_buf_length = self.buf_length & !0b11; 160 | 161 | let drain_length = max_available_self_buf_length.min(actual_max_read_size); 162 | 163 | let decode_length = self 164 | .engine 165 | .decode_slice(&self.buf[self.buf_offset..(self.buf_offset + drain_length)], buf)?; 166 | 167 | buf = &mut buf[decode_length..]; 168 | 169 | self.buf_left_shift(drain_length); 170 | } 171 | 172 | if !buf.is_empty() && self.buf_length >= 4 { 173 | self.drain_block(buf) 174 | } else { 175 | Ok(buf) 176 | } 177 | } 178 | 179 | #[inline] 180 | fn drain_end<'a>(&mut self, mut buf: &'a mut [u8]) -> Result<&'a mut [u8], DecodeSliceError> { 181 | if buf.is_empty() { 182 | return Ok(buf); 183 | } 184 | 185 | if self.temp_length > 0 { 186 | buf = self.drain_temp(buf); 187 | } 188 | 189 | if !buf.is_empty() && self.buf_length > 0 { 190 | self.drain_block(buf) 191 | } else { 192 | Ok(buf) 193 | } 194 | } 195 | } 196 | 197 | impl> Read 198 | for FromBase64Reader 199 | { 200 | fn read(&mut self, mut buf: &mut [u8]) -> Result { 201 | let original_buf_length = buf.len(); 202 | 203 | while self.buf_length < 4 { 204 | match self.inner.read(&mut self.buf[(self.buf_offset + self.buf_length)..]) { 205 | Ok(0) => { 206 | buf = 207 | self.drain_end(buf).map_err(|err| io::Error::new(ErrorKind::Other, err))?; 208 | 209 | return Ok(original_buf_length - buf.len()); 210 | }, 211 | Ok(c) => self.buf_length += c, 212 | Err(ref e) if e.kind() == ErrorKind::Interrupted => {}, 213 | Err(e) => return Err(e), 214 | } 215 | } 216 | 217 | buf = self.drain(buf).map_err(|err| io::Error::new(ErrorKind::Other, err))?; 218 | 219 | Ok(original_buf_length - buf.len()) 220 | } 221 | } 222 | 223 | impl From for FromBase64Reader { 224 | #[inline] 225 | fn from(reader: R) -> Self { 226 | FromBase64Reader::new(reader) 227 | } 228 | } 229 | --------------------------------------------------------------------------------