├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE_APACHE ├── LICENSE_MIT ├── README.md ├── README.tpl ├── ci └── ensure_no_std │ ├── Cargo.toml │ └── src │ └── main.rs ├── docs └── releasing.md ├── rust-toolchain.toml ├── src ├── lib.rs ├── output.rs └── parser.rs └── tests └── tests.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: '0 1 * * *' 12 | 13 | env: 14 | RUSTFLAGS: -D warnings 15 | RUSTDOCFLAGS: -Dwarnings 16 | RUST_BACKTRACE: 1 17 | 18 | defaults: 19 | run: 20 | shell: bash 21 | 22 | jobs: 23 | test: 24 | name: cargo +nightly build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Install latest nightly 30 | uses: dtolnay/rust-toolchain@nightly 31 | with: 32 | toolchain: nightly 33 | targets: thumbv7m-none-eabi 34 | components: rustfmt, clippy 35 | 36 | - uses: Swatinem/rust-cache@v2 37 | 38 | - run: cargo fmt --check 39 | - run: cargo test 40 | - run: cargo clippy --all-features 41 | - run: cargo doc --all-features 42 | 43 | # Ensure that the library doesn't depend on std/alloc. A binary 44 | # that depends on the library is used for this (instead of 45 | # directly build the library), otherwise accidentally depending on 46 | # `alloc` would not result in an error. 47 | - name: cargo build no_std 48 | run: cargo build --target thumbv7m-none-eabi --no-default-features 49 | working-directory: ci/ensure_no_std 50 | 51 | - run: cargo install cargo-readme 52 | - name: Ensure README.md is up-to-date 53 | run: '[ "$(< README.md)" = "$(cargo readme)" ]' 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Release 7 | 8 | permissions: 9 | # Required for pushing the release tag. 10 | contents: write 11 | # Required for crates.io Trusted Publishing. 12 | id-token: write 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: rust-lang/crates-io-auth-action@v1 20 | id: auth 21 | - run: cargo install auto-release 22 | - run: auto-release -p printf-compat --condition subject 23 | env: 24 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.1 (July 20, 2025) 4 | 5 | * Remove dependency on `alloc`. 6 | 7 | * Output `(null)` when a null pointer is formatted with `%s`. 8 | 9 | * Update to edition 2021. 10 | 11 | 12 | ## 0.2.0 (July 14, 2025) 13 | 14 | * Remove `cty` and `cstr_core` dependencies. 15 | 16 | * Improve integer conversions to match C's behavior. 17 | 18 | * Update `itertools` dependency to 0.14.0. 19 | 20 | * Update `bitflags` dependency to 2.9.1. 21 | 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "printf-compat" 3 | description = "printf reimplemented in Rust" 4 | version = "0.2.1" 5 | repository = "https://github.com/lights0123/printf-compat" 6 | authors = ["lights0123 "] 7 | edition = "2021" 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | bitflags = "2.9.1" 13 | itertools = { version = "0.14.0", default-features = false } 14 | 15 | [features] 16 | default = ["std"] 17 | std = [] 18 | -------------------------------------------------------------------------------- /LICENSE_APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Ben Schattinger 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # printf-compat 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/printf-compat.svg)](https://crates.io/crates/printf-compat) 4 | [![Docs.rs](https://docs.rs/printf-compat/badge.svg)](https://docs.rs/printf-compat) 5 | 6 | `printf` reimplemented in Rust 7 | 8 | This is a complete reimplementation of `printf` in Rust, using the unstable 9 | (i.e. **requires a Nightly compiler**) `c_variadic` feature. 10 | 11 | - [Many C][sigrok-log] [libraries][libusb-log] provide a way to provide a 12 | custom log callback. With this crate, you can provide a pure Rust option, 13 | and do whatever you want with it. Log it to the console, store it in a 14 | string, or do anything else. 15 | - If you're writing a Rust-first program for a microcontroller and need to 16 | interface with a C library, you might not *have* a libc and have to 17 | reimplement it yourself. If it uses `printf`, use this crate to easily add 18 | your own output. [`core::fmt`] too big? No problem! Write your own 19 | formatting code, or use a minimal formatting library like [`ufmt`] or 20 | [`defmt`]. Don't need *every* single option given by `printf` format 21 | strings? No problem! Just don't implement it. 22 | - Likewise, if you're using `wasm32-unknown-unknown` instead of emscripten 23 | (as wasm-bindgen is only compatible with the former), you have no libc. If 24 | you want to interface with a C library, you'll have to do it all yourself. 25 | With this crate, that turns into 5 lines instead of hundreds for `printf`. 26 | 27 | ## Benefits 28 | 29 | ### ⚒ Modular 30 | 31 | printf-compat lets you pick how you want to output a message. Use 32 | pre-written adapters for [`fmt::Write`][output::fmt_write] (like a 33 | [`String`]) or [`io::Write`][output::io_write] (like 34 | [`io::stdout()`][std::io::stdout]), or implement your own. 35 | 36 | ### 🔬 Small 37 | 38 | This crate is `no_std` compatible (with `default-features = false`). 39 | The main machinery doesn't require the use of [`core::fmt`], and it can't panic. 40 | 41 | ### 🔒 Safe (as can be) 42 | 43 | Of course, `printf` is *completely* unsafe, as it requires the use of 44 | `va_list`. However, outside of that, all of the actual string parsing is 45 | written in completely safe Rust. No buffer overflow attacks! 46 | 47 | The `n` format specifier, which writes to a user-provided pointer, is 48 | considered a serious security vulnerability if a user-provided string is 49 | ever passed to `printf`. It *is* supported by this crate; however, it 50 | doesn't do anything by default, and you'll have to explicitly do the writing 51 | yourself. 52 | 53 | ### 🧹 Tested 54 | 55 | A wide [test suite] is used to ensure that many different possibilities are 56 | identical to glibc's `printf`. [Differences are 57 | documented][output::fmt_write#differences]. 58 | 59 | ## Getting Started 60 | 61 | Start by adding the unstable feature: 62 | 63 | ```rust 64 | #![feature(c_variadic)] 65 | ``` 66 | 67 | Now, add your function signature: 68 | 69 | ```rust 70 | use core::ffi::{c_char, c_int}; 71 | 72 | #[no_mangle] 73 | unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int { 74 | todo!() 75 | } 76 | ``` 77 | 78 | Think about what you're doing: 79 | 80 | - If you're implenting `printf` *because you don't have one*, you'll want to 81 | call it `printf` and add `#[no_mangle]`. 82 | - Likewise, if you're creating a custom log function for a C library and it 83 | expects to call a globally-defined function, keep `#[no_mangle]` and 84 | rename the function to what it expects. 85 | - On the other hand, if your C library expects you to call a function to 86 | register a callback ([example 1][sigrok-log], [example 2][libusb-log]), 87 | remove `#[no_mangle]`. 88 | 89 | Now, add your logic: 90 | 91 | ```rust 92 | use printf_compat::{format, output}; 93 | let mut s = String::new(); 94 | let bytes_written = format(str, args.as_va_list(), output::fmt_write(&mut s)); 95 | println!("{}", s); 96 | bytes_written 97 | ``` 98 | 99 | Of course, replace [`output::fmt_write`] with whatever you like—some are 100 | provided for you in [`output`]. If you'd like to write your own, follow 101 | their function signature: you need to provide a function to [`format()`] 102 | that takes an [`Argument`] and returns the number of bytes written (although 103 | you don't *need* to if your C library doesn't use it) or -1 if there was an 104 | error. 105 | 106 | [sigrok-log]: https://sigrok.org/api/libsigrok/unstable/a00074.html#ga4240b8fe79be72ef758f40f9acbd4316 107 | [libusb-log]: http://libusb.sourceforge.net/api-1.0/group__libusb__lib.html#ga2efb66b8f16ffb0851f3907794c06e20 108 | [test suite]: https://github.com/lights0123/printf-compat/blob/master/src/tests.rs 109 | [`ufmt`]: https://docs.rs/ufmt/ 110 | [`defmt`]: https://defmt.ferrous-systems.com/ 111 | 112 | License: MIT OR Apache-2.0 113 | 114 | [`core::fmt`]: https://doc.rust-lang.org/core/fmt/index.html 115 | [`String`]: https://doc.rust-lang.org/std/string/struct.String.html 116 | [std::io::stdout]: https://doc.rust-lang.org/std/io/fn.stdout.html 117 | [`std`]: https://doc.rust-lang.org/std/index.html 118 | [`std::os::raw`]: https://doc.rust-lang.org/stable/std/os/raw/index.html 119 | [output::fmt_write]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html 120 | [`output::fmt_write`]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html 121 | [output::fmt_write#differences]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html#differences 122 | [output::io_write]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.io_write.html 123 | [`output`]: https://docs.rs/printf-compat/0.1/printf_compat/output/index.html 124 | [`format()`]: https://docs.rs/printf-compat/0.1/printf_compat/fn.format.html 125 | [`Argument`]: https://docs.rs/printf-compat/0.1/printf_compat/argument/struct.Argument.html 126 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/printf-compat.svg)](https://crates.io/crates/printf-compat) 4 | [![Docs.rs](https://docs.rs/printf-compat/badge.svg)](https://docs.rs/printf-compat) 5 | 6 | {{readme}} 7 | 8 | License: {{license}} 9 | 10 | [`core::fmt`]: https://doc.rust-lang.org/core/fmt/index.html 11 | [`String`]: https://doc.rust-lang.org/std/string/struct.String.html 12 | [std::io::stdout]: https://doc.rust-lang.org/std/io/fn.stdout.html 13 | [`std`]: https://doc.rust-lang.org/std/index.html 14 | [`std::os::raw`]: https://doc.rust-lang.org/stable/std/os/raw/index.html 15 | [output::fmt_write]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html 16 | [`output::fmt_write`]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html 17 | [output::fmt_write#differences]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.fmt_write.html#differences 18 | [output::io_write]: https://docs.rs/printf-compat/0.1/printf_compat/output/fn.io_write.html 19 | [`output`]: https://docs.rs/printf-compat/0.1/printf_compat/output/index.html 20 | [`format()`]: https://docs.rs/printf-compat/0.1/printf_compat/fn.format.html 21 | [`Argument`]: https://docs.rs/printf-compat/0.1/printf_compat/argument/struct.Argument.html 22 | -------------------------------------------------------------------------------- /ci/ensure_no_std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ensure_no_std" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | printf-compat = { path = "../..", default-features = false } 9 | 10 | -------------------------------------------------------------------------------- /ci/ensure_no_std/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![no_std] 3 | 4 | use core::panic::PanicInfo; 5 | use printf_compat as _; // ensure it gets linked 6 | 7 | #[panic_handler] 8 | fn panic(_panic: &PanicInfo<'_>) -> ! { 9 | loop {} 10 | } 11 | -------------------------------------------------------------------------------- /docs/releasing.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | 1. `git checkout master && git pull` 4 | 2. `git checkout -b ` 5 | 3. Update the `version` field in `Cargo.toml`. 6 | 5. Update `CHANGELOG.md`. 7 | 6. Commit `Cargo.toml` and `CHANGELOG.md`. The commit message must start 8 | with `release:`. 9 | 7. Push the branch and create a PR. 10 | 8. Merge the PR in "Rebase and merge" mode (to preserve the commit subject). 11 | 12 | After merging, the new release will automatically be created on 13 | . A git tag will also be created automatically. 14 | 15 | See for more details of how the 16 | release process is implemented. 17 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `printf` reimplemented in Rust 2 | //! 3 | //! This is a complete reimplementation of `printf` in Rust, using the unstable 4 | //! (i.e. **requires a Nightly compiler**) `c_variadic` feature. 5 | //! 6 | //! - [Many C][sigrok-log] [libraries][libusb-log] provide a way to provide a 7 | //! custom log callback. With this crate, you can provide a pure Rust option, 8 | //! and do whatever you want with it. Log it to the console, store it in a 9 | //! string, or do anything else. 10 | //! - If you're writing a Rust-first program for a microcontroller and need to 11 | //! interface with a C library, you might not *have* a libc and have to 12 | //! reimplement it yourself. If it uses `printf`, use this crate to easily add 13 | //! your own output. [`core::fmt`] too big? No problem! Write your own 14 | //! formatting code, or use a minimal formatting library like [`ufmt`] or 15 | //! [`defmt`]. Don't need *every* single option given by `printf` format 16 | //! strings? No problem! Just don't implement it. 17 | //! - Likewise, if you're using `wasm32-unknown-unknown` instead of emscripten 18 | //! (as wasm-bindgen is only compatible with the former), you have no libc. If 19 | //! you want to interface with a C library, you'll have to do it all yourself. 20 | //! With this crate, that turns into 5 lines instead of hundreds for `printf`. 21 | //! 22 | //! # Benefits 23 | //! 24 | //! ## ⚒ Modular 25 | //! 26 | //! printf-compat lets you pick how you want to output a message. Use 27 | //! pre-written adapters for [`fmt::Write`][output::fmt_write] (like a 28 | //! [`String`]) or [`io::Write`][output::io_write] (like 29 | //! [`io::stdout()`][std::io::stdout]), or implement your own. 30 | //! 31 | //! ## 🔬 Small 32 | //! 33 | //! This crate is `no_std` compatible (with `default-features = false`). 34 | //! The main machinery doesn't require the use of [`core::fmt`], and it can't panic. 35 | //! 36 | //! ## 🔒 Safe (as can be) 37 | //! 38 | //! Of course, `printf` is *completely* unsafe, as it requires the use of 39 | //! `va_list`. However, outside of that, all of the actual string parsing is 40 | //! written in completely safe Rust. No buffer overflow attacks! 41 | //! 42 | //! The `n` format specifier, which writes to a user-provided pointer, is 43 | //! considered a serious security vulnerability if a user-provided string is 44 | //! ever passed to `printf`. It *is* supported by this crate; however, it 45 | //! doesn't do anything by default, and you'll have to explicitly do the writing 46 | //! yourself. 47 | //! 48 | //! ## 🧹 Tested 49 | //! 50 | //! A wide [test suite] is used to ensure that many different possibilities are 51 | //! identical to glibc's `printf`. [Differences are 52 | //! documented][output::fmt_write#differences]. 53 | //! 54 | //! # Getting Started 55 | //! 56 | //! Start by adding the unstable feature: 57 | //! 58 | //! ```rust 59 | //! #![feature(c_variadic)] 60 | //! ``` 61 | //! 62 | //! Now, add your function signature: 63 | //! 64 | //! ```rust 65 | //! # #![feature(c_variadic)] 66 | //! use core::ffi::{c_char, c_int}; 67 | //! 68 | //! #[no_mangle] 69 | //! unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int { 70 | //! todo!() 71 | //! } 72 | //! ``` 73 | //! 74 | //! Think about what you're doing: 75 | //! 76 | //! - If you're implenting `printf` *because you don't have one*, you'll want to 77 | //! call it `printf` and add `#[no_mangle]`. 78 | //! - Likewise, if you're creating a custom log function for a C library and it 79 | //! expects to call a globally-defined function, keep `#[no_mangle]` and 80 | //! rename the function to what it expects. 81 | //! - On the other hand, if your C library expects you to call a function to 82 | //! register a callback ([example 1][sigrok-log], [example 2][libusb-log]), 83 | //! remove `#[no_mangle]`. 84 | //! 85 | //! Now, add your logic: 86 | //! 87 | //! ```rust 88 | //! # #![feature(c_variadic)] 89 | //! # use core::ffi::{c_char, c_int}; 90 | //! # #[no_mangle] 91 | //! # unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int { 92 | //! use printf_compat::{format, output}; 93 | //! let mut s = String::new(); 94 | //! let bytes_written = format(str, args.as_va_list(), output::fmt_write(&mut s)); 95 | //! println!("{}", s); 96 | //! bytes_written 97 | //! # } 98 | //! ``` 99 | //! 100 | //! Of course, replace [`output::fmt_write`] with whatever you like—some are 101 | //! provided for you in [`output`]. If you'd like to write your own, follow 102 | //! their function signature: you need to provide a function to [`format()`] 103 | //! that takes an [`Argument`] and returns the number of bytes written (although 104 | //! you don't *need* to if your C library doesn't use it) or -1 if there was an 105 | //! error. 106 | //! 107 | //! [sigrok-log]: https://sigrok.org/api/libsigrok/unstable/a00074.html#ga4240b8fe79be72ef758f40f9acbd4316 108 | //! [libusb-log]: http://libusb.sourceforge.net/api-1.0/group__libusb__lib.html#ga2efb66b8f16ffb0851f3907794c06e20 109 | //! [test suite]: https://github.com/lights0123/printf-compat/blob/master/src/tests.rs 110 | //! [`ufmt`]: https://docs.rs/ufmt/ 111 | //! [`defmt`]: https://defmt.ferrous-systems.com/ 112 | 113 | #![cfg_attr(not(any(test, feature = "std")), no_std)] 114 | #![feature(c_variadic)] 115 | 116 | use core::{ffi::*, fmt}; 117 | 118 | pub mod output; 119 | mod parser; 120 | use argument::*; 121 | pub use parser::format; 122 | pub mod argument { 123 | use super::*; 124 | 125 | bitflags::bitflags! { 126 | /// Flags field. 127 | /// 128 | /// Definitions from 129 | /// [Wikipedia](https://en.wikipedia.org/wiki/Printf_format_string#Flags_field). 130 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 131 | pub struct Flags: u8 { 132 | /// Left-align the output of this placeholder. (The default is to 133 | /// right-align the output.) 134 | const LEFT_ALIGN = 0b00000001; 135 | /// Prepends a plus for positive signed-numeric types. positive = 136 | /// `+`, negative = `-`. 137 | /// 138 | /// (The default doesn't prepend anything in front of positive 139 | /// numbers.) 140 | const PREPEND_PLUS = 0b00000010; 141 | /// Prepends a space for positive signed-numeric types. positive = ` 142 | /// `, negative = `-`. This flag is ignored if the 143 | /// [`PREPEND_PLUS`][Flags::PREPEND_PLUS] flag exists. 144 | /// 145 | /// (The default doesn't prepend anything in front of positive 146 | /// numbers.) 147 | const PREPEND_SPACE = 0b00000100; 148 | /// When the 'width' option is specified, prepends zeros for numeric 149 | /// types. (The default prepends spaces.) 150 | /// 151 | /// For example, `printf("%4X",3)` produces ` 3`, while 152 | /// `printf("%04X",3)` produces `0003`. 153 | const PREPEND_ZERO = 0b00001000; 154 | /// The integer or exponent of a decimal has the thousands grouping 155 | /// separator applied. 156 | const THOUSANDS_GROUPING = 0b00010000; 157 | /// Alternate form: 158 | /// 159 | /// For `g` and `G` types, trailing zeros are not removed. \ 160 | /// For `f`, `F`, `e`, `E`, `g`, `G` types, the output always 161 | /// contains a decimal point. \ For `o`, `x`, `X` types, 162 | /// the text `0`, `0x`, `0X`, respectively, is prepended 163 | /// to non-zero numbers. 164 | const ALTERNATE_FORM = 0b00100000; 165 | } 166 | } 167 | 168 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 169 | pub enum DoubleFormat { 170 | /// `f` 171 | Normal, 172 | /// `F` 173 | UpperNormal, 174 | /// `e` 175 | Scientific, 176 | /// `E` 177 | UpperScientific, 178 | /// `g` 179 | Auto, 180 | /// `G` 181 | UpperAuto, 182 | /// `a` 183 | Hex, 184 | /// `A` 185 | UpperHex, 186 | } 187 | 188 | impl DoubleFormat { 189 | /// If the format is uppercase. 190 | pub fn is_upper(self) -> bool { 191 | use DoubleFormat::*; 192 | matches!(self, UpperNormal | UpperScientific | UpperAuto | UpperHex) 193 | } 194 | 195 | pub fn set_upper(self, upper: bool) -> Self { 196 | use DoubleFormat::*; 197 | match self { 198 | Normal | UpperNormal => { 199 | if upper { 200 | UpperNormal 201 | } else { 202 | Normal 203 | } 204 | } 205 | Scientific | UpperScientific => { 206 | if upper { 207 | UpperScientific 208 | } else { 209 | Scientific 210 | } 211 | } 212 | Auto | UpperAuto => { 213 | if upper { 214 | UpperAuto 215 | } else { 216 | Auto 217 | } 218 | } 219 | Hex | UpperHex => { 220 | if upper { 221 | UpperHex 222 | } else { 223 | Hex 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 231 | #[non_exhaustive] 232 | pub enum SignedInt { 233 | Int(c_int), 234 | Char(c_schar), 235 | Short(c_short), 236 | Long(c_long), 237 | LongLong(c_longlong), 238 | Isize(isize), 239 | } 240 | 241 | impl From for i64 { 242 | fn from(num: SignedInt) -> Self { 243 | // Some casts are only needed on some platforms. 244 | #[allow(clippy::unnecessary_cast)] 245 | match num { 246 | SignedInt::Int(x) => x as i64, 247 | SignedInt::Char(x) => x as i64, 248 | SignedInt::Short(x) => x as i64, 249 | SignedInt::Long(x) => x as i64, 250 | SignedInt::LongLong(x) => x as i64, 251 | SignedInt::Isize(x) => x as i64, 252 | } 253 | } 254 | } 255 | 256 | impl SignedInt { 257 | pub fn is_sign_negative(self) -> bool { 258 | match self { 259 | SignedInt::Int(x) => x < 0, 260 | SignedInt::Char(x) => x < 0, 261 | SignedInt::Short(x) => x < 0, 262 | SignedInt::Long(x) => x < 0, 263 | SignedInt::LongLong(x) => x < 0, 264 | SignedInt::Isize(x) => x < 0, 265 | } 266 | } 267 | } 268 | 269 | impl fmt::Display for SignedInt { 270 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 271 | match self { 272 | SignedInt::Int(x) => fmt::Display::fmt(x, f), 273 | SignedInt::Char(x) => fmt::Display::fmt(x, f), 274 | SignedInt::Short(x) => fmt::Display::fmt(x, f), 275 | SignedInt::Long(x) => fmt::Display::fmt(x, f), 276 | SignedInt::LongLong(x) => fmt::Display::fmt(x, f), 277 | SignedInt::Isize(x) => fmt::Display::fmt(x, f), 278 | } 279 | } 280 | } 281 | 282 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 283 | #[non_exhaustive] 284 | pub enum UnsignedInt { 285 | Int(c_uint), 286 | Char(c_uchar), 287 | Short(c_ushort), 288 | Long(c_ulong), 289 | LongLong(c_ulonglong), 290 | Isize(usize), 291 | } 292 | 293 | impl From for u64 { 294 | fn from(num: UnsignedInt) -> Self { 295 | // Some casts are only needed on some platforms. 296 | #[allow(clippy::unnecessary_cast)] 297 | match num { 298 | UnsignedInt::Int(x) => x as u64, 299 | UnsignedInt::Char(x) => x as u64, 300 | UnsignedInt::Short(x) => x as u64, 301 | UnsignedInt::Long(x) => x as u64, 302 | UnsignedInt::LongLong(x) => x as u64, 303 | UnsignedInt::Isize(x) => x as u64, 304 | } 305 | } 306 | } 307 | 308 | impl fmt::Display for UnsignedInt { 309 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 310 | match self { 311 | UnsignedInt::Int(x) => fmt::Display::fmt(x, f), 312 | UnsignedInt::Char(x) => fmt::Display::fmt(x, f), 313 | UnsignedInt::Short(x) => fmt::Display::fmt(x, f), 314 | UnsignedInt::Long(x) => fmt::Display::fmt(x, f), 315 | UnsignedInt::LongLong(x) => fmt::Display::fmt(x, f), 316 | UnsignedInt::Isize(x) => fmt::Display::fmt(x, f), 317 | } 318 | } 319 | } 320 | 321 | impl fmt::LowerHex for UnsignedInt { 322 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 323 | match self { 324 | UnsignedInt::Int(x) => fmt::LowerHex::fmt(x, f), 325 | UnsignedInt::Char(x) => fmt::LowerHex::fmt(x, f), 326 | UnsignedInt::Short(x) => fmt::LowerHex::fmt(x, f), 327 | UnsignedInt::Long(x) => fmt::LowerHex::fmt(x, f), 328 | UnsignedInt::LongLong(x) => fmt::LowerHex::fmt(x, f), 329 | UnsignedInt::Isize(x) => fmt::LowerHex::fmt(x, f), 330 | } 331 | } 332 | } 333 | 334 | impl fmt::UpperHex for UnsignedInt { 335 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 336 | match self { 337 | UnsignedInt::Int(x) => fmt::UpperHex::fmt(x, f), 338 | UnsignedInt::Char(x) => fmt::UpperHex::fmt(x, f), 339 | UnsignedInt::Short(x) => fmt::UpperHex::fmt(x, f), 340 | UnsignedInt::Long(x) => fmt::UpperHex::fmt(x, f), 341 | UnsignedInt::LongLong(x) => fmt::UpperHex::fmt(x, f), 342 | UnsignedInt::Isize(x) => fmt::UpperHex::fmt(x, f), 343 | } 344 | } 345 | } 346 | 347 | impl fmt::Octal for UnsignedInt { 348 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 349 | match self { 350 | UnsignedInt::Int(x) => fmt::Octal::fmt(x, f), 351 | UnsignedInt::Char(x) => fmt::Octal::fmt(x, f), 352 | UnsignedInt::Short(x) => fmt::Octal::fmt(x, f), 353 | UnsignedInt::Long(x) => fmt::Octal::fmt(x, f), 354 | UnsignedInt::LongLong(x) => fmt::Octal::fmt(x, f), 355 | UnsignedInt::Isize(x) => fmt::Octal::fmt(x, f), 356 | } 357 | } 358 | } 359 | 360 | /// An argument as passed to [`format()`]. 361 | #[derive(Debug, Copy, Clone, PartialEq)] 362 | pub struct Argument<'a> { 363 | pub flags: Flags, 364 | pub width: c_int, 365 | pub precision: Option, 366 | pub specifier: Specifier<'a>, 367 | } 368 | 369 | impl<'a> From> for Argument<'a> { 370 | fn from(specifier: Specifier<'a>) -> Self { 371 | Self { 372 | flags: Flags::empty(), 373 | width: 0, 374 | precision: None, 375 | specifier, 376 | } 377 | } 378 | } 379 | 380 | /// A [format specifier](https://en.wikipedia.org/wiki/Printf_format_string#Type_field). 381 | #[derive(Debug, Copy, Clone, PartialEq)] 382 | #[non_exhaustive] 383 | pub enum Specifier<'a> { 384 | /// `%` 385 | Percent, 386 | /// `d`, `i` 387 | Int(SignedInt), 388 | /// `u` 389 | Uint(UnsignedInt), 390 | /// `o` 391 | Octal(UnsignedInt), 392 | /// `f`, `F`, `e`, `E`, `g`, `G`, `a`, `A` 393 | Double { value: f64, format: DoubleFormat }, 394 | /// string outside of formatting 395 | Bytes(&'a [u8]), 396 | /// `s` 397 | /// 398 | /// The same as [`Bytes`][Specifier::Bytes] but guaranteed to be 399 | /// null-terminated. This can be used for optimizations, where if you 400 | /// need to null terminate a string to print it, you can skip that step. 401 | String(&'a CStr), 402 | /// `c` 403 | Char(u8), 404 | /// `x` 405 | Hex(UnsignedInt), 406 | /// `X` 407 | UpperHex(UnsignedInt), 408 | /// `p` 409 | Pointer(*const ()), 410 | /// `n` 411 | /// 412 | /// # Safety 413 | /// 414 | /// This can be a serious security vulnerability if the format specifier 415 | /// of `printf` is allowed to be user-specified. This shouldn't ever 416 | /// happen, but poorly-written software may do so. 417 | WriteBytesWritten(c_int, *const c_int), 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | //! Various ways to output formatting data. 2 | 3 | use core::cell::Cell; 4 | use core::ffi::*; 5 | use core::fmt; 6 | use core::str::from_utf8; 7 | 8 | #[cfg(feature = "std")] 9 | pub use yes_std::*; 10 | 11 | use crate::{Argument, DoubleFormat, Flags, Specifier}; 12 | 13 | struct DummyWriter(usize); 14 | 15 | impl fmt::Write for DummyWriter { 16 | fn write_str(&mut self, s: &str) -> fmt::Result { 17 | self.0 += s.len(); 18 | Ok(()) 19 | } 20 | } 21 | 22 | struct WriteCounter<'a, T: fmt::Write>(&'a mut T, usize); 23 | 24 | impl<'a, T: fmt::Write> fmt::Write for WriteCounter<'a, T> { 25 | fn write_str(&mut self, s: &str) -> fmt::Result { 26 | self.1 += s.len(); 27 | self.0.write_str(s) 28 | } 29 | } 30 | 31 | fn write_str( 32 | w: &mut impl fmt::Write, 33 | flags: Flags, 34 | width: c_int, 35 | precision: Option, 36 | b: &[u8], 37 | ) -> fmt::Result { 38 | let string = from_utf8(b).map_err(|_| fmt::Error)?; 39 | let precision = precision.unwrap_or(string.len() as c_int); 40 | if flags.contains(Flags::LEFT_ALIGN) { 41 | write!( 42 | w, 43 | "{:1$.prec$}", 44 | string, 45 | width as usize, 46 | prec = precision as usize 47 | ) 48 | } else { 49 | write!( 50 | w, 51 | "{:>1$.prec$}", 52 | string, 53 | width as usize, 54 | prec = precision as usize 55 | ) 56 | } 57 | } 58 | 59 | macro_rules! define_numeric { 60 | ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr) => { 61 | define_numeric!($w, $data, $flags, $width, $precision, "") 62 | }; 63 | ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr, $ty:expr) => {{ 64 | use fmt::Write; 65 | if $flags.contains(Flags::LEFT_ALIGN) { 66 | if $flags.contains(Flags::PREPEND_PLUS) { 67 | write!( 68 | $w, 69 | concat!("{:<+width$.prec$", $ty, "}"), 70 | $data, 71 | width = $width as usize, 72 | prec = $precision as usize 73 | ) 74 | } else if $flags.contains(Flags::PREPEND_SPACE) && !$data.is_sign_negative() { 75 | write!( 76 | $w, 77 | concat!(" {: $width as usize { 119 | $width += 1; 120 | } 121 | write!( 122 | $w, 123 | concat!(" {:0width$.prec$", $ty, "}"), 124 | $data, 125 | width = ($width as usize).wrapping_sub(1), 126 | prec = $precision as usize 127 | ) 128 | } else { 129 | write!( 130 | $w, 131 | concat!("{:0width$.prec$", $ty, "}"), 132 | $data, 133 | width = $width as usize, 134 | prec = $precision as usize 135 | ) 136 | } 137 | } else { 138 | if $flags.contains(Flags::PREPEND_SPACE) && !$data.is_sign_negative() { 139 | let mut d = DummyWriter(0); 140 | let _ = write!( 141 | d, 142 | concat!("{:.prec$", $ty, "}"), 143 | $data, 144 | prec = $precision as usize 145 | ); 146 | if d.0 + 1 > $width as usize { 147 | $width = d.0 as i32 + 1; 148 | } 149 | } 150 | write!( 151 | $w, 152 | concat!("{:width$.prec$", $ty, "}"), 153 | $data, 154 | width = $width as usize, 155 | prec = $precision as usize 156 | ) 157 | } 158 | }}; 159 | } 160 | 161 | macro_rules! define_unumeric { 162 | ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr) => { 163 | define_unumeric!($w, $data, $flags, $width, $precision, "") 164 | }; 165 | ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr, $ty:expr) => {{ 166 | if $flags.contains(Flags::LEFT_ALIGN) { 167 | if $flags.contains(Flags::ALTERNATE_FORM) { 168 | write!( 169 | $w, 170 | concat!("{:<#width$", $ty, "}"), 171 | $data, 172 | width = $width as usize 173 | ) 174 | } else { 175 | write!( 176 | $w, 177 | concat!("{: impl FnMut(Argument) -> c_int + '_ { 233 | use fmt::Write; 234 | move |Argument { 235 | flags, 236 | mut width, 237 | precision, 238 | specifier, 239 | }| { 240 | let mut w = WriteCounter(w, 0); 241 | let w = &mut w; 242 | let res = match specifier { 243 | Specifier::Percent => w.write_char('%'), 244 | Specifier::Bytes(data) => write_str(w, flags, width, precision, data), 245 | Specifier::String(data) => write_str(w, flags, width, precision, data.to_bytes()), 246 | Specifier::Hex(data) => { 247 | define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "x") 248 | } 249 | Specifier::UpperHex(data) => { 250 | define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "X") 251 | } 252 | Specifier::Octal(data) => { 253 | define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "o") 254 | } 255 | Specifier::Uint(data) => { 256 | define_unumeric!(w, data, flags, width, precision.unwrap_or(0)) 257 | } 258 | Specifier::Int(data) => define_numeric!(w, data, flags, width, precision.unwrap_or(0)), 259 | Specifier::Double { value, format } => match format { 260 | DoubleFormat::Normal 261 | | DoubleFormat::UpperNormal 262 | | DoubleFormat::Auto 263 | | DoubleFormat::UpperAuto 264 | | DoubleFormat::Hex 265 | | DoubleFormat::UpperHex => { 266 | define_numeric!(w, value, flags, width, precision.unwrap_or(6)) 267 | } 268 | DoubleFormat::Scientific => { 269 | define_numeric!(w, value, flags, width, precision.unwrap_or(6), "e") 270 | } 271 | DoubleFormat::UpperScientific => { 272 | define_numeric!(w, value, flags, width, precision.unwrap_or(6), "E") 273 | } 274 | }, 275 | Specifier::Char(data) => { 276 | if flags.contains(Flags::LEFT_ALIGN) { 277 | write!(w, "{:width$}", data as char, width = width as usize) 278 | } else { 279 | write!(w, "{:>width$}", data as char, width = width as usize) 280 | } 281 | } 282 | Specifier::Pointer(data) => { 283 | if flags.contains(Flags::LEFT_ALIGN) { 284 | write!(w, "{: Err(Default::default()), 292 | }; 293 | match res { 294 | Ok(_) => w.1 as c_int, 295 | Err(_) => -1, 296 | } 297 | } 298 | } 299 | 300 | /// Returns an object that implements [`Display`][fmt::Display] for safely 301 | /// printing formatting data. This is slightly less performant than using 302 | /// [`fmt_write`], but may be the only option. 303 | /// 304 | /// This shares the same caveats as [`fmt_write`]. 305 | /// 306 | /// # Safety 307 | /// 308 | /// [`VaList`]s are *very* unsafe. The passed `format` and `args` parameter must be a valid [`printf` format string](http://www.cplusplus.com/reference/cstdio/printf/). 309 | pub unsafe fn display<'a, 'b>( 310 | format: *const c_char, 311 | va_list: VaList<'a, 'b>, 312 | ) -> VaListDisplay<'a, 'b> { 313 | VaListDisplay { 314 | format, 315 | va_list, 316 | written: Cell::new(0), 317 | } 318 | } 319 | 320 | /// Helper struct created by [`display`] for safely printing `printf`-style 321 | /// formatting with [`format!`] and `{}`. This can be used with anything that 322 | /// uses [`format_args!`], such as [`println!`] or the `log` crate. 323 | /// 324 | /// ```rust 325 | /// #![feature(c_variadic)] 326 | /// 327 | /// use core::ffi::{c_char, c_int}; 328 | /// 329 | /// #[no_mangle] 330 | /// unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int { 331 | /// let format = printf_compat::output::display(str, args.as_va_list()); 332 | /// println!("{}", format); 333 | /// format.bytes_written() 334 | /// } 335 | /// ``` 336 | pub struct VaListDisplay<'a, 'b> { 337 | format: *const c_char, 338 | va_list: VaList<'a, 'b>, 339 | written: Cell, 340 | } 341 | 342 | impl VaListDisplay<'_, '_> { 343 | /// Get the number of bytes written, or 0 if there was an error. 344 | pub fn bytes_written(&self) -> c_int { 345 | self.written.get() 346 | } 347 | } 348 | 349 | impl<'a, 'b> fmt::Display for VaListDisplay<'a, 'b> { 350 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 351 | unsafe { 352 | let bytes = crate::format(self.format, self.va_list.clone().as_va_list(), fmt_write(f)); 353 | self.written.set(bytes); 354 | if bytes < 0 { 355 | Err(fmt::Error) 356 | } else { 357 | Ok(()) 358 | } 359 | } 360 | } 361 | } 362 | 363 | #[cfg(feature = "std")] 364 | mod yes_std { 365 | use std::io; 366 | 367 | use super::*; 368 | 369 | struct FmtWriter(T, io::Result<()>); 370 | 371 | impl fmt::Write for FmtWriter { 372 | fn write_str(&mut self, s: &str) -> fmt::Result { 373 | match self.0.write_all(s.as_bytes()) { 374 | Ok(()) => Ok(()), 375 | Err(e) => { 376 | self.1 = Err(e); 377 | Err(fmt::Error) 378 | } 379 | } 380 | } 381 | } 382 | 383 | struct IoWriteCounter<'a, T: io::Write>(&'a mut T, usize); 384 | 385 | impl<'a, T: io::Write> io::Write for IoWriteCounter<'a, T> { 386 | fn write(&mut self, buf: &[u8]) -> io::Result { 387 | self.0.write_all(buf)?; 388 | self.1 += buf.len(); 389 | Ok(buf.len()) 390 | } 391 | 392 | fn flush(&mut self) -> io::Result<()> { 393 | self.0.flush() 394 | } 395 | } 396 | 397 | fn write_bytes( 398 | w: &mut impl io::Write, 399 | flags: Flags, 400 | width: c_int, 401 | precision: Option, 402 | b: &[u8], 403 | ) -> io::Result<()> { 404 | let precision = precision.unwrap_or(b.len() as c_int); 405 | let b = b.get(..(b.len().min(precision as usize))).unwrap_or(&[]); 406 | 407 | if flags.contains(Flags::LEFT_ALIGN) { 408 | w.write_all(b)?; 409 | for _ in 0..((width as usize).saturating_sub(b.len())) { 410 | w.write_all(b" ")?; 411 | } 412 | Ok(()) 413 | } else { 414 | for _ in 0..((width as usize).saturating_sub(b.len())) { 415 | w.write_all(b" ")?; 416 | } 417 | w.write_all(b) 418 | } 419 | } 420 | 421 | /// Write to a struct that implements [`io::Write`]. 422 | /// 423 | /// This shares the same caveats as [`fmt_write`], except that non-UTF-8 424 | /// data is supported. 425 | pub fn io_write(w: &mut impl io::Write) -> impl FnMut(Argument) -> c_int + '_ { 426 | use io::Write; 427 | move |Argument { 428 | flags, 429 | width, 430 | precision, 431 | specifier, 432 | }| { 433 | let mut w = IoWriteCounter(w, 0); 434 | let mut w = &mut w; 435 | let res = match specifier { 436 | Specifier::Percent => w.write_all(b"%"), 437 | Specifier::Bytes(data) => write_bytes(w, flags, width, precision, data), 438 | Specifier::String(data) => write_bytes(w, flags, width, precision, data.to_bytes()), 439 | _ => { 440 | let mut writer = FmtWriter(&mut w, Ok(())); 441 | fmt_write(&mut writer)(Argument { 442 | flags, 443 | width, 444 | precision, 445 | specifier, 446 | }); 447 | writer.1 448 | } 449 | }; 450 | match res { 451 | Ok(_) => w.1 as c_int, 452 | Err(_) => -1, 453 | } 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use core::ffi::*; 2 | 3 | use crate::{Argument, DoubleFormat, Flags, SignedInt, Specifier, UnsignedInt}; 4 | use itertools::Itertools; 5 | 6 | fn next_char(sub: &[u8]) -> &[u8] { 7 | sub.get(1..).unwrap_or(&[]) 8 | } 9 | 10 | /// Parse the [Flags field](https://en.wikipedia.org/wiki/Printf_format_string#Flags_field). 11 | fn parse_flags(mut sub: &[u8]) -> (Flags, &[u8]) { 12 | let mut flags: Flags = Flags::empty(); 13 | while let Some(&ch) = sub.first() { 14 | flags.insert(match ch { 15 | b'-' => Flags::LEFT_ALIGN, 16 | b'+' => Flags::PREPEND_PLUS, 17 | b' ' => Flags::PREPEND_SPACE, 18 | b'0' => Flags::PREPEND_ZERO, 19 | b'\'' => Flags::THOUSANDS_GROUPING, 20 | b'#' => Flags::ALTERNATE_FORM, 21 | _ => break, 22 | }); 23 | sub = next_char(sub) 24 | } 25 | (flags, sub) 26 | } 27 | 28 | /// Parse the [Width field](https://en.wikipedia.org/wiki/Printf_format_string#Width_field). 29 | unsafe fn parse_width<'a>(mut sub: &'a [u8], args: &mut VaList) -> (c_int, &'a [u8]) { 30 | let mut width: c_int = 0; 31 | if sub.first() == Some(&b'*') { 32 | return (args.arg(), next_char(sub)); 33 | } 34 | while let Some(&ch) = sub.first() { 35 | match ch { 36 | // https://rust-malaysia.github.io/code/2020/07/11/faster-integer-parsing.html#the-bytes-solution 37 | b'0'..=b'9' => width = width * 10 + (ch & 0x0f) as c_int, 38 | _ => break, 39 | } 40 | sub = next_char(sub); 41 | } 42 | (width, sub) 43 | } 44 | 45 | /// Parse the [Precision field](https://en.wikipedia.org/wiki/Printf_format_string#Precision_field). 46 | unsafe fn parse_precision<'a>(sub: &'a [u8], args: &mut VaList) -> (Option, &'a [u8]) { 47 | match sub.first() { 48 | Some(&b'.') => { 49 | let (prec, sub) = parse_width(next_char(sub), args); 50 | (Some(prec), sub) 51 | } 52 | _ => (None, sub), 53 | } 54 | } 55 | 56 | #[derive(Debug, Copy, Clone)] 57 | enum Length { 58 | Int, 59 | /// `hh` 60 | Char, 61 | /// `h` 62 | Short, 63 | /// `l` 64 | Long, 65 | /// `ll` 66 | LongLong, 67 | /// `z` 68 | Usize, 69 | /// `t` 70 | Isize, 71 | } 72 | 73 | impl Length { 74 | unsafe fn parse_signed(self, args: &mut VaList) -> SignedInt { 75 | match self { 76 | Length::Int => SignedInt::Int(args.arg()), 77 | Length::Char => SignedInt::Char(args.arg::() as i8), 78 | Length::Short => SignedInt::Short(args.arg::() as i16), 79 | Length::Long => SignedInt::Long(args.arg()), 80 | Length::LongLong => SignedInt::LongLong(args.arg()), 81 | // for some reason, these exist as different options, yet produce the same output 82 | Length::Usize | Length::Isize => SignedInt::Isize(args.arg()), 83 | } 84 | } 85 | unsafe fn parse_unsigned(self, args: &mut VaList) -> UnsignedInt { 86 | match self { 87 | Length::Int => UnsignedInt::Int(args.arg()), 88 | Length::Char => UnsignedInt::Char(args.arg::() as u8), 89 | Length::Short => UnsignedInt::Short(args.arg::() as u16), 90 | Length::Long => UnsignedInt::Long(args.arg()), 91 | Length::LongLong => UnsignedInt::LongLong(args.arg()), 92 | // for some reason, these exist as different options, yet produce the same output 93 | Length::Usize | Length::Isize => UnsignedInt::Isize(args.arg()), 94 | } 95 | } 96 | } 97 | 98 | /// Parse the [Length field](https://en.wikipedia.org/wiki/Printf_format_string#Length_field). 99 | fn parse_length(sub: &[u8]) -> (Length, &[u8]) { 100 | match sub.first().copied() { 101 | Some(b'h') => match sub.get(1).copied() { 102 | Some(b'h') => (Length::Char, sub.get(2..).unwrap_or(&[])), 103 | _ => (Length::Short, next_char(sub)), 104 | }, 105 | Some(b'l') => match sub.get(1).copied() { 106 | Some(b'l') => (Length::LongLong, sub.get(2..).unwrap_or(&[])), 107 | _ => (Length::Long, next_char(sub)), 108 | }, 109 | Some(b'z') => (Length::Usize, next_char(sub)), 110 | Some(b't') => (Length::Isize, next_char(sub)), 111 | _ => (Length::Int, sub), 112 | } 113 | } 114 | 115 | /// Parse a format parameter and write it somewhere. 116 | /// 117 | /// # Safety 118 | /// 119 | /// [`VaList`]s are *very* unsafe. The passed `format` and `args` parameter must be a valid [`printf` format string](http://www.cplusplus.com/reference/cstdio/printf/). 120 | pub unsafe fn format( 121 | format: *const c_char, 122 | mut args: VaList, 123 | mut handler: impl FnMut(Argument) -> c_int, 124 | ) -> c_int { 125 | let str = CStr::from_ptr(format).to_bytes(); 126 | let mut iter = str.split(|&c| c == b'%'); 127 | let mut written = 0; 128 | 129 | macro_rules! err { 130 | ($ex: expr) => {{ 131 | let res = $ex; 132 | if res < 0 { 133 | return -1; 134 | } else { 135 | written += res; 136 | } 137 | }}; 138 | } 139 | if let Some(begin) = iter.next() { 140 | err!(handler(Specifier::Bytes(begin).into())); 141 | } 142 | let mut last_was_percent = false; 143 | for (sub, next) in iter.map(Some).chain(core::iter::once(None)).tuple_windows() { 144 | let sub = match sub { 145 | Some(sub) => sub, 146 | None => break, 147 | }; 148 | if last_was_percent { 149 | err!(handler(Specifier::Bytes(sub).into())); 150 | last_was_percent = false; 151 | continue; 152 | } 153 | let (flags, sub) = parse_flags(sub); 154 | let (width, sub) = parse_width(sub, &mut args); 155 | let (precision, sub) = parse_precision(sub, &mut args); 156 | let (length, sub) = parse_length(sub); 157 | let ch = sub 158 | .first() 159 | .unwrap_or(if next.is_some() { &b'%' } else { &0 }); 160 | err!(handler(Argument { 161 | flags, 162 | width, 163 | precision, 164 | specifier: match ch { 165 | b'%' => { 166 | last_was_percent = true; 167 | Specifier::Percent 168 | } 169 | b'd' | b'i' => Specifier::Int(length.parse_signed(&mut args)), 170 | b'x' => Specifier::Hex(length.parse_unsigned(&mut args)), 171 | b'X' => Specifier::UpperHex(length.parse_unsigned(&mut args)), 172 | b'u' => Specifier::Uint(length.parse_unsigned(&mut args)), 173 | b'o' => Specifier::Octal(length.parse_unsigned(&mut args)), 174 | b'f' | b'F' => Specifier::Double { 175 | value: args.arg(), 176 | format: DoubleFormat::Normal.set_upper(ch.is_ascii_uppercase()), 177 | }, 178 | b'e' | b'E' => Specifier::Double { 179 | value: args.arg(), 180 | format: DoubleFormat::Scientific.set_upper(ch.is_ascii_uppercase()), 181 | }, 182 | b'g' | b'G' => Specifier::Double { 183 | value: args.arg(), 184 | format: DoubleFormat::Auto.set_upper(ch.is_ascii_uppercase()), 185 | }, 186 | b'a' | b'A' => Specifier::Double { 187 | value: args.arg(), 188 | format: DoubleFormat::Hex.set_upper(ch.is_ascii_uppercase()), 189 | }, 190 | b's' => { 191 | let arg: *mut c_char = args.arg(); 192 | // As a common extension supported by glibc, musl, and 193 | // others, format a NULL pointer as "(null)". 194 | if arg.is_null() { 195 | Specifier::Bytes(b"(null)") 196 | } else { 197 | Specifier::String(CStr::from_ptr(arg)) 198 | } 199 | } 200 | b'c' => Specifier::Char(args.arg::() as u8), 201 | b'p' => Specifier::Pointer(args.arg()), 202 | b'n' => Specifier::WriteBytesWritten(written, args.arg()), 203 | _ => return -1, 204 | }, 205 | })); 206 | err!(handler(Specifier::Bytes(next_char(sub)).into())); 207 | } 208 | written 209 | } 210 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | #![feature(c_variadic)] 2 | 3 | use core::{ffi::*, ptr::null_mut}; 4 | 5 | extern "C" { 6 | fn asprintf(s: *mut *mut c_char, format: *const c_char, ...) -> c_int; 7 | fn free(p: *mut c_void); 8 | } 9 | 10 | unsafe extern "C" fn rust_fmt(str: *const c_char, mut args: ...) -> Box<(c_int, String)> { 11 | let mut s = String::new(); 12 | let bytes_written = printf_compat::format( 13 | str, 14 | args.clone().as_va_list(), 15 | printf_compat::output::fmt_write(&mut s), 16 | ); 17 | assert!(bytes_written >= 0); 18 | let mut s2 = std::io::Cursor::new(vec![]); 19 | assert_eq!( 20 | bytes_written, 21 | printf_compat::format( 22 | str, 23 | args.as_va_list(), 24 | printf_compat::output::io_write(&mut s2), 25 | ) 26 | ); 27 | assert_eq!(s.as_bytes(), s2.get_ref()); 28 | Box::new((bytes_written, s)) 29 | } 30 | 31 | macro_rules! c_fmt { 32 | ($format:literal $(, $p:expr)*) => {{ 33 | let mut ptr = null_mut(); 34 | let bytes_written = asprintf(&mut ptr, $format.as_ptr() $(, $p)*); 35 | assert!(bytes_written >= 0); 36 | let s: String = CStr::from_ptr(ptr).to_string_lossy().into(); 37 | free(ptr.cast()); 38 | (bytes_written, s) 39 | }}; 40 | } 41 | 42 | /// Assert that `rust_fmt` produces the same output as C's `asprintf`, 43 | /// and that both match the `expected` literal. 44 | /// 45 | /// This takes a format literal, followed by optional printf arguments, 46 | /// followed by `=>` and then the `expected` output. 47 | /// 48 | /// Example usage: 49 | /// 50 | /// ``` 51 | /// assert_eq_fmt!(c"%d %d", 1, 2 => "1 2"); 52 | /// ``` 53 | macro_rules! assert_eq_fmt { 54 | ($format:literal $(, $p:expr)* => $expected:literal) => { 55 | let (bytes_written, s) = c_fmt!($format $(, $p)*); 56 | assert_eq!(s, $expected); 57 | assert_eq!((bytes_written, s), *rust_fmt($format.as_ptr().cast(), $($p),*)); 58 | assert_eq!(usize::try_from(bytes_written).unwrap(), $expected.len()); 59 | }; 60 | } 61 | 62 | /// Assert that a format string fails to parse. This checks that both 63 | /// C's `asprintf` and `printf_compat::format` return -1. 64 | fn assert_fmt_err(fmt: &CStr) { 65 | let mut ptr = null_mut(); 66 | let bytes_written = unsafe { asprintf(&mut ptr, fmt.as_ptr()) }; 67 | assert_eq!(bytes_written, -1, "asprintf parse unexpectedly succeeded"); 68 | 69 | unsafe extern "C" fn format(str: *const c_char, args: ...) -> c_int { 70 | let mut s = String::new(); 71 | let bytes_written = printf_compat::format( 72 | str, 73 | args.clone().as_va_list(), 74 | printf_compat::output::fmt_write(&mut s), 75 | ); 76 | bytes_written 77 | } 78 | let bytes_written = unsafe { format(fmt.as_ptr()) }; 79 | assert_eq!( 80 | bytes_written, -1, 81 | "printf_compat::output parse unexpectedly succeeded" 82 | ); 83 | } 84 | 85 | #[test] 86 | fn test_plain() { 87 | unsafe { 88 | assert_eq_fmt!(c"abc" => "abc"); 89 | assert_eq_fmt!(c"" => ""); 90 | assert_eq_fmt!(c"%%" => "%"); 91 | assert_eq_fmt!(c"%% def" => "% def"); 92 | assert_eq_fmt!(c"abc %%" => "abc %"); 93 | assert_eq_fmt!(c"abc %% def" => "abc % def"); 94 | assert_eq_fmt!(c"abc %%%% def" => "abc %% def"); 95 | assert_eq_fmt!(c"%%%%%%" => "%%%"); 96 | } 97 | } 98 | 99 | #[test] 100 | fn test_str() { 101 | unsafe { 102 | assert_eq_fmt!(c"hello %s", c"world" => "hello world"); 103 | assert_eq_fmt!(c"hello %%%s", c"world" => "hello %world"); 104 | assert_eq_fmt!(c"%10s", c"world" => " world"); 105 | assert_eq_fmt!(c"%.4s", c"world" => "worl"); 106 | assert_eq_fmt!(c"%10.4s", c"world" => " worl"); 107 | assert_eq_fmt!(c"%-10.4s", c"world" => "worl "); 108 | assert_eq_fmt!(c"%-10s", c"world" => "world "); 109 | assert_eq_fmt!(c"%s", null_mut::() => "(null)"); 110 | } 111 | } 112 | 113 | #[test] 114 | fn test_int() { 115 | unsafe { 116 | assert_eq_fmt!(c"% 0*i", 17, 23125 => " 0000000000023125"); 117 | assert_eq_fmt!(c"% 010i", 23125 => " 000023125"); 118 | assert_eq_fmt!(c"% 10i", 23125 => " 23125"); 119 | assert_eq_fmt!(c"% 5i", 23125 => " 23125"); 120 | assert_eq_fmt!(c"% 4i", 23125 => " 23125"); 121 | assert_eq_fmt!(c"%- 010i", 23125 => " 23125 "); 122 | assert_eq_fmt!(c"%- 10i", 23125 => " 23125 "); 123 | assert_eq_fmt!(c"%- 5i", 23125 => " 23125"); 124 | assert_eq_fmt!(c"%- 4i", 23125 => " 23125"); 125 | assert_eq_fmt!(c"%+ 010i", 23125 => "+000023125"); 126 | assert_eq_fmt!(c"%+ 10i", 23125 => " +23125"); 127 | assert_eq_fmt!(c"%+ 5i", 23125 => "+23125"); 128 | assert_eq_fmt!(c"%+ 4i", 23125 => "+23125"); 129 | assert_eq_fmt!(c"%-010i", 23125 => "23125 "); 130 | assert_eq_fmt!(c"%-10i", 23125 => "23125 "); 131 | assert_eq_fmt!(c"%-5i", 23125 => "23125"); 132 | assert_eq_fmt!(c"%-4i", 23125 => "23125"); 133 | } 134 | } 135 | 136 | #[test] 137 | fn test_int_length_signed() { 138 | unsafe { 139 | assert_eq_fmt!(c"%hhi", -125 => "-125"); 140 | assert_eq_fmt!(c"%hi", -23125 => "-23125"); 141 | assert_eq_fmt!(c"%li", -211_126_823_125i64 => "-211126823125"); 142 | assert_eq_fmt!(c"%lli", -211_126_823_125i64 => "-211126823125"); 143 | assert_eq_fmt!(c"%ti", -211_126_823_125isize => "-211126823125"); 144 | assert_eq_fmt!(c"%zi", 211_126_823_125usize => "211126823125"); 145 | 146 | assert_eq_fmt!(c"% 5hhi", -125 => " -125"); 147 | assert_eq_fmt!(c"% 7hi", -23125 => " -23125"); 148 | assert_eq_fmt!(c"% 14li", -211_126_823_125i64 => " -211126823125"); 149 | assert_eq_fmt!(c"% 14lli", -211_126_823_125i64 => " -211126823125"); 150 | assert_eq_fmt!(c"% 14ti", -211_126_823_125isize => " -211126823125"); 151 | assert_eq_fmt!(c"% 13zi", 211_126_823_125usize => " 211126823125"); 152 | } 153 | } 154 | 155 | #[test] 156 | fn test_int_length_unsigned() { 157 | unsafe { 158 | assert_eq_fmt!(c"%hhu", 125 => "125"); 159 | assert_eq_fmt!(c"%hu", 23125 => "23125"); 160 | assert_eq_fmt!(c"%lu", 211_126_823_125u64 => "211126823125"); 161 | assert_eq_fmt!(c"%llu", 211_126_823_125u64 => "211126823125"); 162 | assert_eq_fmt!(c"%tu", 211_126_823_125isize => "211126823125"); 163 | assert_eq_fmt!(c"%zu", 211_126_823_125usize => "211126823125"); 164 | } 165 | } 166 | 167 | #[test] 168 | fn test_octal() { 169 | unsafe { 170 | assert_eq_fmt!(c"% 010o", 23125 => "0000055125"); 171 | assert_eq_fmt!(c"% 10o", 23125 => " 55125"); 172 | assert_eq_fmt!(c"% 5o", 23125 => "55125"); 173 | assert_eq_fmt!(c"% 4o", 23125 => "55125"); 174 | assert_eq_fmt!(c"%- 010o", 23125 => "55125 "); 175 | assert_eq_fmt!(c"%- 10o", 23125 => "55125 "); 176 | assert_eq_fmt!(c"%- 5o", 23125 => "55125"); 177 | assert_eq_fmt!(c"%- 4o", 23125 => "55125"); 178 | assert_eq_fmt!(c"%+ 010o", 23125 => "0000055125"); 179 | assert_eq_fmt!(c"%+ 10o", 23125 => " 55125"); 180 | assert_eq_fmt!(c"%+ 5o", 23125 => "55125"); 181 | assert_eq_fmt!(c"%+ 4o", 23125 => "55125"); 182 | assert_eq_fmt!(c"%-010o", 23125 => "55125 "); 183 | assert_eq_fmt!(c"%-10o", 23125 => "55125 "); 184 | assert_eq_fmt!(c"%-5o", 23125 => "55125"); 185 | assert_eq_fmt!(c"%-4o", 23125 => "55125"); 186 | } 187 | } 188 | 189 | #[test] 190 | fn test_hex() { 191 | unsafe { 192 | assert_eq_fmt!(c"% 010x", 23125 => "0000005a55"); 193 | assert_eq_fmt!(c"% 10x", 23125 => " 5a55"); 194 | assert_eq_fmt!(c"% 5x", 23125 => " 5a55"); 195 | assert_eq_fmt!(c"% 4x", 23125 => "5a55"); 196 | assert_eq_fmt!(c"%- 010x", 23125 => "5a55 "); 197 | assert_eq_fmt!(c"%- 10x", 23125 => "5a55 "); 198 | assert_eq_fmt!(c"%- 5x", 23125 => "5a55 "); 199 | assert_eq_fmt!(c"%- 4x", 23125 => "5a55"); 200 | assert_eq_fmt!(c"%+ 010x", 23125 => "0000005a55"); 201 | assert_eq_fmt!(c"%+ 10x", 23125 => " 5a55"); 202 | assert_eq_fmt!(c"%+ 5x", 23125 => " 5a55"); 203 | assert_eq_fmt!(c"%+ 4x", 23125 => "5a55"); 204 | assert_eq_fmt!(c"%-010x", 23125 => "5a55 "); 205 | assert_eq_fmt!(c"%-10x", 23125 => "5a55 "); 206 | assert_eq_fmt!(c"%-5x", 23125 => "5a55 "); 207 | assert_eq_fmt!(c"%-4x", 23125 => "5a55"); 208 | 209 | assert_eq_fmt!(c"%# 010x", 23125 => "0x00005a55"); 210 | assert_eq_fmt!(c"%# 10x", 23125 => " 0x5a55"); 211 | assert_eq_fmt!(c"%# 5x", 23125 => "0x5a55"); 212 | assert_eq_fmt!(c"%# 4x", 23125 => "0x5a55"); 213 | assert_eq_fmt!(c"%#- 010x", 23125 => "0x5a55 "); 214 | assert_eq_fmt!(c"%#- 10x", 23125 => "0x5a55 "); 215 | assert_eq_fmt!(c"%#- 5x", 23125 => "0x5a55"); 216 | assert_eq_fmt!(c"%#- 4x", 23125 => "0x5a55"); 217 | assert_eq_fmt!(c"%#+ 010x", 23125 => "0x00005a55"); 218 | assert_eq_fmt!(c"%#+ 10x", 23125 => " 0x5a55"); 219 | assert_eq_fmt!(c"%#+ 5x", 23125 => "0x5a55"); 220 | assert_eq_fmt!(c"%#+ 4x", 23125 => "0x5a55"); 221 | assert_eq_fmt!(c"%#-010x", 23125 => "0x5a55 "); 222 | assert_eq_fmt!(c"%#-10x", 23125 => "0x5a55 "); 223 | assert_eq_fmt!(c"%#-5x", 23125 => "0x5a55"); 224 | assert_eq_fmt!(c"%#-4x", 23125 => "0x5a55"); 225 | 226 | assert_eq_fmt!(c"% 010X", 23125 => "0000005A55"); 227 | assert_eq_fmt!(c"% 10X", 23125 => " 5A55"); 228 | assert_eq_fmt!(c"% 5X", 23125 => " 5A55"); 229 | assert_eq_fmt!(c"% 4X", 23125 => "5A55"); 230 | assert_eq_fmt!(c"%- 010X", 23125 => "5A55 "); 231 | assert_eq_fmt!(c"%- 10X", 23125 => "5A55 "); 232 | assert_eq_fmt!(c"%- 5X", 23125 => "5A55 "); 233 | assert_eq_fmt!(c"%- 4X", 23125 => "5A55"); 234 | assert_eq_fmt!(c"%+ 010X", 23125 => "0000005A55"); 235 | assert_eq_fmt!(c"%+ 10X", 23125 => " 5A55"); 236 | assert_eq_fmt!(c"%+ 5X", 23125 => " 5A55"); 237 | assert_eq_fmt!(c"%+ 4X", 23125 => "5A55"); 238 | assert_eq_fmt!(c"%-010X", 23125 => "5A55 "); 239 | assert_eq_fmt!(c"%-10X", 23125 => "5A55 "); 240 | assert_eq_fmt!(c"%-5X", 23125 => "5A55 "); 241 | assert_eq_fmt!(c"%-4X", 23125 => "5A55"); 242 | } 243 | } 244 | 245 | #[test] 246 | fn test_float() { 247 | unsafe { 248 | assert_eq_fmt!(c"%f", 1234f64 => "1234.000000"); 249 | assert_eq_fmt!(c"%.5f", 1234f64 => "1234.00000"); 250 | assert_eq_fmt!(c"%.*f", 1234f64, 3 => "1234.000"); 251 | } 252 | } 253 | 254 | #[test] 255 | fn test_char() { 256 | unsafe { 257 | assert_eq_fmt!(c"%c", b'a' as c_int => "a"); 258 | assert_eq_fmt!(c"%10c", b'a' as c_int => " a"); 259 | assert_eq_fmt!(c"%-10c", b'a' as c_int => "a "); 260 | } 261 | } 262 | 263 | #[test] 264 | fn test_errors() { 265 | assert_fmt_err(c"%"); 266 | assert_fmt_err(c"%1"); 267 | } 268 | --------------------------------------------------------------------------------