Browse Source

Initial Commit

lights0123 4 years ago
commit
1aee15fa48
11 changed files with 1651 additions and 0 deletions
  1. 49 0
      .github/workflows/ci.yml
  2. 2 0
      .gitignore
  3. 21 0
      Cargo.toml
  4. 176 0
      LICENSE_APACHE
  5. 25 0
      LICENSE_MIT
  6. 113 0
      README.md
  7. 8 0
      README.tpl
  8. 419 0
      src/lib.rs
  9. 439 0
      src/output.rs
  10. 203 0
      src/parser.rs
  11. 196 0
      src/tests.rs

+ 49 - 0
.github/workflows/ci.yml

@@ -0,0 +1,49 @@
+name: CI
+
+on:
+  pull_request:
+  push:
+  schedule:
+    - cron: '0 1 * * *'
+
+env:
+  RUSTFLAGS: -D warnings
+  RUST_BACKTRACE: 1
+
+defaults:
+  run:
+    shell: bash
+
+jobs:
+  test:
+    name: cargo +nightly build
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/bin
+            ~/.cargo/registry
+            ~/.cargo/git
+            ~/.cargo/.crates.toml
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - name: Install latest nightly
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: nightly
+          target: thumbv7m-none-eabi
+          default: true
+          components: rustfmt, clippy
+      - run: cargo install cargo-readme
+      - run: cargo test
+      - name: cargo build no_std
+        run: cargo build --target thumbv7m-none-eabi --no-default-features
+      - name: cargo clippy
+        uses: actions-rs/clippy-check@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          args: --all-features
+      - name: Ensure README.md is updated
+        run: [ "$(< README.md)" = "$(cargo readme)" ]

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 21 - 0
Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "printf-compat"
+description = "printf reimplemented in Rust"
+version = "0.1.0"
+repository = "https://github.com/lights0123/printf-compat"
+authors = ["lights0123 <developer@lights0123.com>"]
+edition = "2018"
+license = "MIT OR Apache-2.0"
+readme = "README.md"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+cty = "0.2.1"
+cstr_core = "0.2.2"
+bitflags = "1.2.1"
+itertools = { version = "0.9.0", default-features = false }
+
+[features]
+default = ["std"]
+std = []

+ 176 - 0
LICENSE_APACHE

@@ -0,0 +1,176 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS

+ 25 - 0
LICENSE_MIT

@@ -0,0 +1,25 @@
+Copyright (c) 2018 Ben Schattinger <developer@lights0123.com>
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.

+ 113 - 0
README.md

@@ -0,0 +1,113 @@
+# printf-compat
+
+[![Crates.io](https://img.shields.io/crates/v/printf-compat.svg)](https://crates.io/crates/printf-compat)
+[![Docs.rs](https://docs.rs/printf-compat/badge.svg)](https://docs.rs/printf-compat)
+
+`printf` reimplemented in Rust
+
+This is a complete reimplementation of `printf` in Rust, using the unstable
+(i.e. **requires a Nightly compiler**) `c_variadic` feature.
+
+- [Many C][sigrok-log] [libraries][libusb-log] provide a way to provide a
+  custom log callback. With this crate, you can provide a pure Rust option,
+  and do whatever you want with it. Log it to the console,
+- If you're writing a Rust-first program for a microcontroller and need to
+  interface with a C library, you might not *have* a libc and have to
+  reimplement it yourself. If it uses `printf`, use this crate to easily add
+  your own output. [`core::fmt`] too big? No problem! Write your own
+  formatting code, or use a minimal formatting library like [`ufmt`] or
+  [`defmt`]. Don't need *every* single option given by `printf` format
+  strings? No problem! Just don't implement it.
+- Likewise, if you're using `wasm32-unknown-unknown` instead of emscripten
+  (as wasm-bindgen is only compatible with the former), you have no libc. If
+  you want to interface with a C library, you'll have to do it all yourself.
+  With this crate, that turns into 5 lines instead of hundreds for `printf`.
+
+## Benefits
+
+### ⚒ Modular
+
+printf-compat lets you pick how you want to output a message. Use
+pre-written adapters for [`fmt::Write`][output::fmt_write] (like a
+[`String`]) or [`io::Write`][output::io_write] (like
+[`io::stdout()`][std::io::stdout]), or implement your own.
+
+### 🔬 Small
+
+This crate is `no_std` compatible (`printf-compat = { version = "0.1",
+default-features = false }` in your Cargo.toml). The main machinery doesn't
+require the use of [`core::fmt`], and it can't panic.
+
+### 🔒 Safe (as can be)
+
+Of course, `printf` is *completely* unsafe, as it requires the use of
+`va_list`. However, outside of that, all of the actual string parsing is
+written in completely safe Rust. No buffer overflow attacks!
+
+The `n` format specifier, which writes to a user-provided pointer, is
+considered a serious security vulnerability if a user-provided string is
+ever passed to `printf`. It *is* supported by this crate; however, it
+doesn't do anything by default, and you'll have to explicitly do the writing
+yourself.
+
+### 🧹 Tested
+
+A wide [test suite] is used to ensure that many different possibilities are
+identical to glibc's `printf`. [Differences are documented][output::fmt_write#differences].
+
+## Getting Started
+
+Start by adding the unstable feature:
+
+```rust
+#![feature(c_variadic)]
+```
+
+Now, add your function signature:
+
+```rust
+use cty::{c_char, c_int};
+
+#[no_mangle]
+unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int {
+    todo!()
+}
+```
+
+If you have access to [`std`], i.e. not an embedded platform, you can use
+[`std::os::raw`] instead of [`cty`]. Also, think about what you're doing:
+
+- If you're implenting `printf` *because you don't have one*, you'll want to
+  call it `printf` and add `#[no_mangle]`.
+- Likewise, if you're creating a custom log function for a C library and it
+  expects to call a globally-defined function, keep `#[no_mangle]` and
+  rename the function to what it expects.
+- On the other hand, if your C library expects you to call a function to
+  register a callback ([example 1][sigrok-log], [example 2][libusb-log]),
+  remove `#[no_mangle]`.
+
+Now, add your logic:
+
+```rust
+#
+use printf_compat::{format, output};
+let mut s = String::new();
+let bytes_written = format(str, args.as_va_list(), output::fmt_write(&mut s));
+println!("{}", s);
+bytes_written
+```
+
+Of course, replace [`output::fmt_write`] with whatever you like—some are
+provided for you in [`output`]. If you'd like to write your own, follow
+their function signature: you need to provide a function to [`format()`]
+that takes an [`Argument`] and returns the number of bytes written (although
+you don't *need* to if your C library doesn't use it) or -1 if there was an
+error.
+
+[sigrok-log]: https://sigrok.org/api/libsigrok/unstable/a00074.html#ga4240b8fe79be72ef758f40f9acbd4316
+[libusb-log]: http://libusb.sourceforge.net/api-1.0/group__libusb__lib.html#ga2efb66b8f16ffb0851f3907794c06e20
+[test suite]: https://github.com/lights0123/printf-compat/blob/master/src/tests.rs
+[`ufmt`]: https://docs.rs/ufmt/
+[`defmt`]: https://defmt.ferrous-systems.com/
+
+License: MIT OR Apache-2.0

+ 8 - 0
README.tpl

@@ -0,0 +1,8 @@
+# {{crate}}
+
+[![Crates.io](https://img.shields.io/crates/v/printf-compat.svg)](https://crates.io/crates/printf-compat)
+[![Docs.rs](https://docs.rs/printf-compat/badge.svg)](https://docs.rs/printf-compat)
+
+{{readme}}
+
+License: {{license}}

+ 419 - 0
src/lib.rs

@@ -0,0 +1,419 @@
+//! `printf` reimplemented in Rust
+//!
+//! This is a complete reimplementation of `printf` in Rust, using the unstable
+//! (i.e. **requires a Nightly compiler**) `c_variadic` feature.
+//!
+//! - [Many C][sigrok-log] [libraries][libusb-log] provide a way to provide a
+//!   custom log callback. With this crate, you can provide a pure Rust option,
+//!   and do whatever you want with it. Log it to the console,
+//! - If you're writing a Rust-first program for a microcontroller and need to
+//!   interface with a C library, you might not *have* a libc and have to
+//!   reimplement it yourself. If it uses `printf`, use this crate to easily add
+//!   your own output. [`core::fmt`] too big? No problem! Write your own
+//!   formatting code, or use a minimal formatting library like [`ufmt`] or
+//!   [`defmt`]. Don't need *every* single option given by `printf` format
+//!   strings? No problem! Just don't implement it.
+//! - Likewise, if you're using `wasm32-unknown-unknown` instead of emscripten
+//!   (as wasm-bindgen is only compatible with the former), you have no libc. If
+//!   you want to interface with a C library, you'll have to do it all yourself.
+//!   With this crate, that turns into 5 lines instead of hundreds for `printf`.
+//!
+//! # Benefits
+//!
+//! ## ⚒ Modular
+//!
+//! printf-compat lets you pick how you want to output a message. Use
+//! pre-written adapters for [`fmt::Write`][output::fmt_write] (like a
+//! [`String`]) or [`io::Write`][output::io_write] (like
+//! [`io::stdout()`][std::io::stdout]), or implement your own.
+//!
+//! ## 🔬 Small
+//!
+//! This crate is `no_std` compatible (`printf-compat = { version = "0.1",
+//! default-features = false }` in your Cargo.toml). The main machinery doesn't
+//! require the use of [`core::fmt`], and it can't panic.
+//!
+//! ## 🔒 Safe (as can be)
+//!
+//! Of course, `printf` is *completely* unsafe, as it requires the use of
+//! `va_list`. However, outside of that, all of the actual string parsing is
+//! written in completely safe Rust. No buffer overflow attacks!
+//!
+//! The `n` format specifier, which writes to a user-provided pointer, is
+//! considered a serious security vulnerability if a user-provided string is
+//! ever passed to `printf`. It *is* supported by this crate; however, it
+//! doesn't do anything by default, and you'll have to explicitly do the writing
+//! yourself.
+//!
+//! ## 🧹 Tested
+//!
+//! A wide [test suite] is used to ensure that many different possibilities are
+//! identical to glibc's `printf`. [Differences are documented][output::fmt_write#differences].
+//!
+//! # Getting Started
+//!
+//! Start by adding the unstable feature:
+//!
+//! ```rust
+//! #![feature(c_variadic)]
+//! ```
+//!
+//! Now, add your function signature:
+//!
+//! ```rust
+//! # #![feature(c_variadic)]
+//! use cty::{c_char, c_int};
+//!
+//! #[no_mangle]
+//! unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int {
+//!     todo!()
+//! }
+//! ```
+//!
+//! If you have access to [`std`], i.e. not an embedded platform, you can use
+//! [`std::os::raw`] instead of [`cty`]. Also, think about what you're doing:
+//!
+//! - If you're implenting `printf` *because you don't have one*, you'll want to
+//!   call it `printf` and add `#[no_mangle]`.
+//! - Likewise, if you're creating a custom log function for a C library and it
+//!   expects to call a globally-defined function, keep `#[no_mangle]` and
+//!   rename the function to what it expects.
+//! - On the other hand, if your C library expects you to call a function to
+//!   register a callback ([example 1][sigrok-log], [example 2][libusb-log]),
+//!   remove `#[no_mangle]`.
+//!
+//! Now, add your logic:
+//!
+//! ```rust
+//! # #![feature(c_variadic)]
+//! # use cty::{c_char, c_int};
+//! #
+//! # #[no_mangle]
+//! # unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int {
+//! use printf_compat::{format, output};
+//! let mut s = String::new();
+//! let bytes_written = format(str, args.as_va_list(), output::fmt_write(&mut s));
+//! println!("{}", s);
+//! bytes_written
+//! # }
+//! ```
+//!
+//! Of course, replace [`output::fmt_write`] with whatever you like—some are
+//! provided for you in [`output`]. If you'd like to write your own, follow
+//! their function signature: you need to provide a function to [`format()`]
+//! that takes an [`Argument`] and returns the number of bytes written (although
+//! you don't *need* to if your C library doesn't use it) or -1 if there was an
+//! error.
+//!
+//! [sigrok-log]: https://sigrok.org/api/libsigrok/unstable/a00074.html#ga4240b8fe79be72ef758f40f9acbd4316
+//! [libusb-log]: http://libusb.sourceforge.net/api-1.0/group__libusb__lib.html#ga2efb66b8f16ffb0851f3907794c06e20
+//! [test suite]: https://github.com/lights0123/printf-compat/blob/master/src/tests.rs
+//! [`ufmt`]: https://docs.rs/ufmt/
+//! [`defmt`]: https://defmt.ferrous-systems.com/
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![feature(c_variadic)]
+
+use core::fmt;
+use cstr_core::CStr;
+use cty::*;
+
+pub mod output;
+mod parser;
+#[cfg(test)]
+mod tests;
+use argument::*;
+pub use parser::format;
+pub mod argument {
+    use super::*;
+
+    bitflags::bitflags! {
+        /// Flags field.
+        ///
+        /// Definitions from
+        /// [Wikipedia](https://en.wikipedia.org/wiki/Printf_format_string#Flags_field).
+        pub struct Flags: u8 {
+            /// Left-align the output of this placeholder. (The default is to
+            /// right-align the output.)
+            const LEFT_ALIGN = 0b00000001;
+            /// Prepends a plus for positive signed-numeric types. positive =
+            /// `+`, negative = `-`.
+            ///
+            /// (The default doesn't prepend anything in front of positive
+            /// numbers.)
+            const PREPEND_PLUS = 0b00000010;
+            /// Prepends a space for positive signed-numeric types. positive = `
+            /// `, negative = `-`. This flag is ignored if the
+            /// [`PREPEND_PLUS`][Flags::PREPEND_PLUS] flag exists.
+            ///
+            /// (The default doesn't prepend anything in front of positive
+            /// numbers.)
+            const PREPEND_SPACE = 0b00000100;
+            /// When the 'width' option is specified, prepends zeros for numeric
+            /// types. (The default prepends spaces.)
+            ///
+            /// For example, `printf("%4X",3)` produces `   3`, while
+            /// `printf("%04X",3)` produces `0003`.
+            const PREPEND_ZERO = 0b00001000;
+            /// The integer or exponent of a decimal has the thousands grouping
+            /// separator applied.
+            const THOUSANDS_GROUPING = 0b00010000;
+            /// Alternate form:
+            ///
+            /// For `g` and `G` types, trailing zeros are not removed. \
+            /// For `f`, `F`, `e`, `E`, `g`, `G` types, the output always
+            /// contains a decimal point. \ For `o`, `x`, `X` types,
+            /// the text `0`, `0x`, `0X`, respectively, is prepended
+            /// to non-zero numbers.
+            const ALTERNATE_FORM = 0b00100000;
+        }
+    }
+
+    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+    pub enum DoubleFormat {
+        /// `f`
+        Normal,
+        /// `F`
+        UpperNormal,
+        /// `e`
+        Scientific,
+        /// `E`
+        UpperScientific,
+        /// `g`
+        Auto,
+        /// `G`
+        UpperAuto,
+        /// `a`
+        Hex,
+        /// `A`
+        UpperHex,
+    }
+
+    impl DoubleFormat {
+        /// If the format is uppercase.
+        pub fn is_upper(self) -> bool {
+            use DoubleFormat::*;
+            matches!(self, UpperNormal | UpperScientific | UpperAuto | UpperHex)
+        }
+
+        pub fn set_upper(self, upper: bool) -> Self {
+            use DoubleFormat::*;
+            match self {
+                Normal | UpperNormal => {
+                    if upper {
+                        UpperNormal
+                    } else {
+                        Normal
+                    }
+                }
+                Scientific | UpperScientific => {
+                    if upper {
+                        UpperScientific
+                    } else {
+                        Scientific
+                    }
+                }
+                Auto | UpperAuto => {
+                    if upper {
+                        UpperAuto
+                    } else {
+                        Auto
+                    }
+                }
+                Hex | UpperHex => {
+                    if upper {
+                        UpperHex
+                    } else {
+                        Hex
+                    }
+                }
+            }
+        }
+    }
+
+    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+    #[non_exhaustive]
+    pub enum SignedInt {
+        Int(c_int),
+        Char(c_schar),
+        Short(c_short),
+        Long(c_long),
+        LongLong(c_longlong),
+        Isize(isize),
+    }
+
+    impl From<SignedInt> for i64 {
+        fn from(num: SignedInt) -> Self {
+            match num {
+                SignedInt::Int(x) => x as i64,
+                SignedInt::Char(x) => x as i64,
+                SignedInt::Short(x) => x as i64,
+                SignedInt::Long(x) => x as i64,
+                SignedInt::LongLong(x) => x as i64,
+                SignedInt::Isize(x) => x as i64,
+            }
+        }
+    }
+
+    impl SignedInt {
+        pub fn is_sign_negative(self) -> bool {
+            match self {
+                SignedInt::Int(x) => x < 0,
+                SignedInt::Char(x) => x < 0,
+                SignedInt::Short(x) => x < 0,
+                SignedInt::Long(x) => x < 0,
+                SignedInt::LongLong(x) => x < 0,
+                SignedInt::Isize(x) => x < 0,
+            }
+        }
+    }
+
+    impl fmt::Display for SignedInt {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            match self {
+                SignedInt::Int(x) => fmt::Display::fmt(x, f),
+                SignedInt::Char(x) => fmt::Display::fmt(x, f),
+                SignedInt::Short(x) => fmt::Display::fmt(x, f),
+                SignedInt::Long(x) => fmt::Display::fmt(x, f),
+                SignedInt::LongLong(x) => fmt::Display::fmt(x, f),
+                SignedInt::Isize(x) => fmt::Display::fmt(x, f),
+            }
+        }
+    }
+
+    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+    #[non_exhaustive]
+    pub enum UnsignedInt {
+        Int(c_uint),
+        Char(c_uchar),
+        Short(c_ushort),
+        Long(c_ulong),
+        LongLong(c_ulonglong),
+        Isize(usize),
+    }
+
+    impl From<UnsignedInt> for u64 {
+        fn from(num: UnsignedInt) -> Self {
+            match num {
+                UnsignedInt::Int(x) => x as u64,
+                UnsignedInt::Char(x) => x as u64,
+                UnsignedInt::Short(x) => x as u64,
+                UnsignedInt::Long(x) => x as u64,
+                UnsignedInt::LongLong(x) => x as u64,
+                UnsignedInt::Isize(x) => x as u64,
+            }
+        }
+    }
+
+    impl fmt::Display for UnsignedInt {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            match self {
+                UnsignedInt::Int(x) => fmt::Display::fmt(x, f),
+                UnsignedInt::Char(x) => fmt::Display::fmt(x, f),
+                UnsignedInt::Short(x) => fmt::Display::fmt(x, f),
+                UnsignedInt::Long(x) => fmt::Display::fmt(x, f),
+                UnsignedInt::LongLong(x) => fmt::Display::fmt(x, f),
+                UnsignedInt::Isize(x) => fmt::Display::fmt(x, f),
+            }
+        }
+    }
+
+    impl fmt::LowerHex for UnsignedInt {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            match self {
+                UnsignedInt::Int(x) => fmt::LowerHex::fmt(x, f),
+                UnsignedInt::Char(x) => fmt::LowerHex::fmt(x, f),
+                UnsignedInt::Short(x) => fmt::LowerHex::fmt(x, f),
+                UnsignedInt::Long(x) => fmt::LowerHex::fmt(x, f),
+                UnsignedInt::LongLong(x) => fmt::LowerHex::fmt(x, f),
+                UnsignedInt::Isize(x) => fmt::LowerHex::fmt(x, f),
+            }
+        }
+    }
+
+    impl fmt::UpperHex for UnsignedInt {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            match self {
+                UnsignedInt::Int(x) => fmt::UpperHex::fmt(x, f),
+                UnsignedInt::Char(x) => fmt::UpperHex::fmt(x, f),
+                UnsignedInt::Short(x) => fmt::UpperHex::fmt(x, f),
+                UnsignedInt::Long(x) => fmt::UpperHex::fmt(x, f),
+                UnsignedInt::LongLong(x) => fmt::UpperHex::fmt(x, f),
+                UnsignedInt::Isize(x) => fmt::UpperHex::fmt(x, f),
+            }
+        }
+    }
+
+    impl fmt::Octal for UnsignedInt {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            match self {
+                UnsignedInt::Int(x) => fmt::Octal::fmt(x, f),
+                UnsignedInt::Char(x) => fmt::Octal::fmt(x, f),
+                UnsignedInt::Short(x) => fmt::Octal::fmt(x, f),
+                UnsignedInt::Long(x) => fmt::Octal::fmt(x, f),
+                UnsignedInt::LongLong(x) => fmt::Octal::fmt(x, f),
+                UnsignedInt::Isize(x) => fmt::Octal::fmt(x, f),
+            }
+        }
+    }
+
+    /// An argument as passed to [`format`][crate::format].
+    #[derive(Debug, Copy, Clone, PartialEq)]
+    pub struct Argument<'a> {
+        pub flags: Flags,
+        pub width: c_int,
+        pub precision: Option<c_int>,
+        pub specifier: Specifier<'a>,
+    }
+
+    impl<'a> From<Specifier<'a>> for Argument<'a> {
+        fn from(specifier: Specifier<'a>) -> Self {
+            Self {
+                flags: Flags::empty(),
+                width: 0,
+                precision: None,
+                specifier,
+            }
+        }
+    }
+
+    /// A [format specifier](https://en.wikipedia.org/wiki/Printf_format_string#Type_field).
+    #[derive(Debug, Copy, Clone, PartialEq)]
+    #[non_exhaustive]
+    pub enum Specifier<'a> {
+        /// `%`
+        Percent,
+        /// `d`, `i`
+        Int(SignedInt),
+        /// `u`
+        Uint(UnsignedInt),
+        /// `o`
+        Octal(UnsignedInt),
+        /// `f`, `F`, `e`, `E`, `g`, `G`, `a`, `A`
+        Double { value: f64, format: DoubleFormat },
+        /// string outside of formatting
+        Bytes(&'a [u8]),
+        /// `s`
+        ///
+        /// The same as [`Bytes`][Specifier::Bytes] but guaranteed to be
+        /// null-terminated. This can be used for optimizations, where if you
+        /// need to null terminate a string to print it, you can skip that step.
+        String(&'a CStr),
+        /// `c`
+        Char(u8),
+        /// `x`
+        Hex(UnsignedInt),
+        /// `X`
+        UpperHex(UnsignedInt),
+        /// `p`
+        Pointer(*const ()),
+        /// `n`
+        ///
+        /// # Safety
+        ///
+        /// This can be a serious security vulnerability if the format specifier
+        /// of `printf` is allowed to be user-specified. This shouldn't ever
+        /// happen, but poorly-written software may do so.
+        WriteBytesWritten(c_int, *const c_int),
+    }
+}

+ 439 - 0
src/output.rs

@@ -0,0 +1,439 @@
+//! Various ways to output formatting data.
+
+use core::cell::Cell;
+use core::ffi::VaList;
+use core::fmt;
+use core::str::from_utf8;
+
+use cty::*;
+
+#[cfg(feature = "std")]
+pub use yes_std::*;
+
+use crate::{Argument, DoubleFormat, Flags, Specifier};
+
+struct DummyWriter(usize);
+
+impl fmt::Write for DummyWriter {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.0 += s.len();
+        Ok(())
+    }
+}
+
+fn write_str(
+    w: &mut impl fmt::Write,
+    flags: Flags,
+    width: c_int,
+    precision: Option<c_int>,
+    b: &[u8],
+) -> fmt::Result {
+    let string = from_utf8(b).map_err(|_| fmt::Error)?;
+    let precision = precision.unwrap_or(string.len() as c_int);
+    if flags.contains(Flags::LEFT_ALIGN) {
+        write!(
+            w,
+            "{:1$.prec$}",
+            string,
+            width as usize,
+            prec = precision as usize
+        )
+    } else {
+        write!(
+            w,
+            "{:>1$.prec$}",
+            string,
+            width as usize,
+            prec = precision as usize
+        )
+    }
+}
+
+macro_rules! define_numeric {
+    ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr) => {
+        define_numeric!($w, $data, $flags, $width, $precision, "")
+    };
+    ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr, $ty:expr) => {{
+        use fmt::Write;
+        if $flags.contains(Flags::LEFT_ALIGN) {
+            if $flags.contains(Flags::PREPEND_PLUS) {
+                write!(
+                    $w,
+                    concat!("{:<+width$.prec$", $ty, "}"),
+                    $data,
+                    width = $width as usize,
+                    prec = $precision as usize
+                )
+            } else if $flags.contains(Flags::PREPEND_SPACE) && !$data.is_sign_negative() {
+                write!(
+                    $w,
+                    concat!(" {:<width$.prec$", $ty, "}"),
+                    $data,
+                    width = ($width as usize).wrapping_sub(1),
+                    prec = $precision as usize
+                )
+            } else {
+                write!(
+                    $w,
+                    concat!("{:<width$.prec$", $ty, "}"),
+                    $data,
+                    width = $width as usize,
+                    prec = $precision as usize
+                )
+            }
+        } else if $flags.contains(Flags::PREPEND_PLUS) {
+            if $flags.contains(Flags::PREPEND_ZERO) {
+                write!(
+                    $w,
+                    concat!("{:+0width$.prec$", $ty, "}"),
+                    $data,
+                    width = $width as usize,
+                    prec = $precision as usize
+                )
+            } else {
+                write!(
+                    $w,
+                    concat!("{:+width$.prec$", $ty, "}"),
+                    $data,
+                    width = $width as usize,
+                    prec = $precision as usize
+                )
+            }
+        } else if $flags.contains(Flags::PREPEND_ZERO) {
+            if $flags.contains(Flags::PREPEND_SPACE) && !$data.is_sign_negative() {
+                let mut d = DummyWriter(0);
+                let _ = write!(
+                    d,
+                    concat!("{:.prec$", $ty, "}"),
+                    $data,
+                    prec = $precision as usize
+                );
+                if d.0 + 1 > $width as usize {
+                    $width += 1;
+                }
+                write!(
+                    $w,
+                    concat!(" {:0width$.prec$", $ty, "}"),
+                    $data,
+                    width = ($width as usize).wrapping_sub(1),
+                    prec = $precision as usize
+                )
+            } else {
+                write!(
+                    $w,
+                    concat!("{:0width$.prec$", $ty, "}"),
+                    $data,
+                    width = $width as usize,
+                    prec = $precision as usize
+                )
+            }
+        } else {
+            if $flags.contains(Flags::PREPEND_SPACE) && !$data.is_sign_negative() {
+                let mut d = DummyWriter(0);
+                let _ = write!(
+                    d,
+                    concat!("{:.prec$", $ty, "}"),
+                    $data,
+                    prec = $precision as usize
+                );
+                if d.0 + 1 > $width as usize {
+                    $width = d.0 as i32 + 1;
+                }
+            }
+            write!(
+                $w,
+                concat!("{:width$.prec$", $ty, "}"),
+                $data,
+                width = $width as usize,
+                prec = $precision as usize
+            )
+        }
+    }};
+}
+
+macro_rules! define_unumeric {
+    ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr) => {
+        define_unumeric!($w, $data, $flags, $width, $precision, "")
+    };
+    ($w: expr, $data: expr, $flags: expr, $width: expr, $precision: expr, $ty:expr) => {{
+        if $flags.contains(Flags::LEFT_ALIGN) {
+            if $flags.contains(Flags::ALTERNATE_FORM) {
+                write!(
+                    $w,
+                    concat!("{:<#width$", $ty, "}"),
+                    $data,
+                    width = $width as usize
+                )
+            } else {
+                write!(
+                    $w,
+                    concat!("{:<width$", $ty, "}"),
+                    $data,
+                    width = $width as usize
+                )
+            }
+        } else if $flags.contains(Flags::ALTERNATE_FORM) {
+            if $flags.contains(Flags::PREPEND_ZERO) {
+                write!(
+                    $w,
+                    concat!("{:#0width$", $ty, "}"),
+                    $data,
+                    width = $width as usize
+                )
+            } else {
+                write!(
+                    $w,
+                    concat!("{:#width$", $ty, "}"),
+                    $data,
+                    width = $width as usize
+                )
+            }
+        } else if $flags.contains(Flags::PREPEND_ZERO) {
+            write!(
+                $w,
+                concat!("{:0width$", $ty, "}"),
+                $data,
+                width = $width as usize
+            )
+        } else {
+            write!(
+                $w,
+                concat!("{:width$", $ty, "}"),
+                $data,
+                width = $width as usize
+            )
+        }
+    }};
+}
+
+/// Write to a struct that implements [`fmt::Write`].
+///
+/// # Differences
+///
+/// There are a few differences from standard printf format:
+///
+/// - only valid UTF-8 data can be printed.
+/// - an `X` format specifier with a `#` flag prints the hex data in uppercase,
+///   but the leading `0x` is still lowercase
+/// - an `o` format specifier with a `#` flag precedes the number with an `o`
+///   instead of `0`
+/// - `g`/`G` (shorted floating point) is aliased to `f`/`F`` (decimal floating
+///   point)
+/// - same for `a`/`A` (hex floating point)
+/// - the number of bytes written is not counted
+/// - the `n` format specifier, [`Specifier::WriteBytesWritten`], is not
+///   implemented and will cause an error if encountered.
+pub fn fmt_write(w: &mut impl fmt::Write) -> impl FnMut(Argument) -> c_int + '_ {
+    move |Argument {
+              flags,
+              mut width,
+              precision,
+              specifier,
+          }| {
+        let res = match specifier {
+            Specifier::Percent => w.write_char('%'),
+            Specifier::Bytes(data) => write_str(w, flags, width, precision, data),
+            Specifier::String(data) => write_str(w, flags, width, precision, data.to_bytes()),
+            Specifier::Hex(data) => {
+                define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "x")
+            }
+            Specifier::UpperHex(data) => {
+                define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "X")
+            }
+            Specifier::Octal(data) => {
+                define_unumeric!(w, data, flags, width, precision.unwrap_or(0), "o")
+            }
+            Specifier::Uint(data) => {
+                define_unumeric!(w, data, flags, width, precision.unwrap_or(0))
+            }
+            Specifier::Int(data) => define_numeric!(w, data, flags, width, precision.unwrap_or(0)),
+            Specifier::Double { value, format } => match format {
+                DoubleFormat::Normal
+                | DoubleFormat::UpperNormal
+                | DoubleFormat::Auto
+                | DoubleFormat::UpperAuto
+                | DoubleFormat::Hex
+                | DoubleFormat::UpperHex => {
+                    define_numeric!(w, value, flags, width, precision.unwrap_or(6))
+                }
+                DoubleFormat::Scientific => {
+                    define_numeric!(w, value, flags, width, precision.unwrap_or(6), "e")
+                }
+                DoubleFormat::UpperScientific => {
+                    define_numeric!(w, value, flags, width, precision.unwrap_or(6), "E")
+                }
+            },
+            Specifier::Char(data) => {
+                if flags.contains(Flags::LEFT_ALIGN) {
+                    write!(w, "{:width$}", data as char, width = width as usize)
+                } else {
+                    write!(w, "{:>width$}", data as char, width = width as usize)
+                }
+            }
+            Specifier::Pointer(data) => {
+                if flags.contains(Flags::LEFT_ALIGN) {
+                    write!(w, "{:<width$p}", data, width = width as usize)
+                } else if flags.contains(Flags::PREPEND_ZERO) {
+                    write!(w, "{:0width$p}", data, width = width as usize)
+                } else {
+                    write!(w, "{:width$p}", data, width = width as usize)
+                }
+            }
+            Specifier::WriteBytesWritten(_, _) => Err(Default::default()),
+        };
+        match res {
+            Ok(_) => 0,
+            Err(_) => -1,
+        }
+    }
+}
+
+/// Returns an object that implements [`Display`][fmt::Display] for safely
+/// printing formatting data. This is slightly less performant than using
+/// [`fmt_write`], but may be the only option.
+///
+/// This shares the same caveats as [`fmt_write`].
+pub unsafe fn display<'a, 'b>(
+    format: *const c_char,
+    va_list: VaList<'a, 'b>,
+) -> VaListDisplay<'a, 'b> {
+    VaListDisplay {
+        format,
+        va_list,
+        written: Cell::new(0),
+    }
+}
+
+/// Helper struct created by [`display`] for safely printing `printf`-style
+/// formatting with [`format!`] and `{}`. This can be used with anything that
+/// uses [`format_args!`], such as [`println!`] or the `log` crate.
+///
+/// ```rust
+/// #![feature(c_variadic)]
+///
+/// use cty::{c_char, c_int};
+///
+/// #[no_mangle]
+/// unsafe extern "C" fn c_library_print(str: *const c_char, mut args: ...) -> c_int {
+///     let format = printf_compat::output::display(str, args.as_va_list());
+///     println!("{}", format);
+///     format.bytes_written()
+/// }
+/// ```
+///
+/// If you have access to [`std`], i.e. not an embedded platform, you can use
+/// [`std::os::raw`] instead of [`cty`].
+pub struct VaListDisplay<'a, 'b> {
+    format: *const c_char,
+    va_list: VaList<'a, 'b>,
+    written: Cell<c_int>,
+}
+
+impl VaListDisplay<'_, '_> {
+    /// Get the number of bytes written, or 0 if there was an error.
+    pub fn bytes_written(&self) -> c_int {
+        self.written.get()
+    }
+}
+
+impl<'a, 'b> fmt::Display for VaListDisplay<'a, 'b> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        unsafe {
+            let bytes = crate::format(self.format, self.va_list.clone().as_va_list(), fmt_write(f));
+            self.written.set(bytes);
+            if bytes < 0 {
+                Err(fmt::Error)
+            } else {
+                Ok(())
+            }
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+mod yes_std {
+    use std::io;
+
+    use super::*;
+
+    struct FmtWriter<T: io::Write>(T, io::Result<()>);
+
+    impl<T: io::Write> fmt::Write for FmtWriter<T> {
+        fn write_str(&mut self, s: &str) -> fmt::Result {
+            match self.0.write_all(s.as_bytes()) {
+                Ok(()) => Ok(()),
+                Err(e) => {
+                    self.1 = Err(e);
+                    Err(fmt::Error)
+                }
+            }
+        }
+
+        fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
+            match self.0.write_fmt(args) {
+                Ok(()) => Ok(()),
+                Err(e) => {
+                    self.1 = Err(e);
+                    Err(fmt::Error)
+                }
+            }
+        }
+    }
+
+    fn write_bytes(
+        w: &mut impl io::Write,
+        flags: Flags,
+        width: c_int,
+        precision: Option<c_int>,
+        b: &[u8],
+    ) -> io::Result<()> {
+        let precision = precision.unwrap_or(b.len() as c_int);
+        let b = b.get(..(b.len().min(precision as usize))).unwrap_or(&[]);
+
+        if flags.contains(Flags::LEFT_ALIGN) {
+            w.write_all(b)?;
+            for _ in 0..((width as usize).saturating_sub(b.len())) {
+                w.write_all(b" ")?;
+            }
+            Ok(())
+        } else {
+            for _ in 0..((width as usize).saturating_sub(b.len())) {
+                w.write_all(b" ")?;
+            }
+            w.write_all(b)
+        }
+    }
+
+    /// Write to a struct that implements [`io::Write`].
+    ///
+    /// This shares the same caveats as [`fmt_write`], except that non-UTF-8
+    /// data is supported.
+    pub fn io_write(mut w: &mut impl io::Write) -> impl FnMut(Argument) -> c_int + '_ {
+        move |Argument {
+                  flags,
+                  width,
+                  precision,
+                  specifier,
+              }| {
+            let res = match specifier {
+                Specifier::Percent => w.write_all(b"%"),
+                Specifier::Bytes(data) => write_bytes(w, flags, width, precision, data),
+                Specifier::String(data) => write_bytes(w, flags, width, precision, data.to_bytes()),
+                _ => {
+                    let mut writer = FmtWriter(&mut w, Ok(()));
+                    fmt_write(&mut writer)(Argument {
+                        flags,
+                        width,
+                        precision,
+                        specifier,
+                    });
+                    writer.1
+                }
+            };
+            match res {
+                Ok(_) => 0,
+                Err(_) => -1,
+            }
+        }
+    }
+}

+ 203 - 0
src/parser.rs

@@ -0,0 +1,203 @@
+use core::ffi::VaList;
+
+use cstr_core::CStr;
+use cty::*;
+
+use crate::{Argument, DoubleFormat, Flags, SignedInt, Specifier, UnsignedInt};
+use itertools::Itertools;
+
+fn next_char(sub: &[u8]) -> &[u8] {
+    sub.get(1..).unwrap_or(&[])
+}
+
+/// Parse the [Flags field](https://en.wikipedia.org/wiki/Printf_format_string#Flags_field).
+fn parse_flags(mut sub: &[u8]) -> (Flags, &[u8]) {
+    let mut flags: Flags = Flags::empty();
+    while let Some(&ch) = sub.get(0) {
+        flags.insert(match ch {
+            b'-' => Flags::LEFT_ALIGN,
+            b'+' => Flags::PREPEND_PLUS,
+            b' ' => Flags::PREPEND_SPACE,
+            b'0' => Flags::PREPEND_ZERO,
+            b'\'' => Flags::THOUSANDS_GROUPING,
+            b'#' => Flags::ALTERNATE_FORM,
+            _ => break,
+        });
+        sub = next_char(sub)
+    }
+    (flags, sub)
+}
+
+/// Parse the [Width field](https://en.wikipedia.org/wiki/Printf_format_string#Width_field).
+unsafe fn parse_width<'a>(mut sub: &'a [u8], args: &mut VaList) -> (c_int, &'a [u8]) {
+    let mut width: c_int = 0;
+    if sub.get(0) == Some(&b'*') {
+        return (args.arg(), next_char(sub));
+    }
+    while let Some(&ch) = sub.get(0) {
+        match ch {
+            // https://rust-malaysia.github.io/code/2020/07/11/faster-integer-parsing.html#the-bytes-solution
+            b'0'..=b'9' => width = width * 10 + (ch & 0x0f) as c_int,
+            _ => break,
+        }
+        sub = next_char(sub);
+    }
+    (width, sub)
+}
+
+/// Parse the [Precision field](https://en.wikipedia.org/wiki/Printf_format_string#Precision_field).
+unsafe fn parse_precision<'a>(sub: &'a [u8], args: &mut VaList) -> (Option<c_int>, &'a [u8]) {
+    match sub.get(0) {
+        Some(&b'.') => {
+            let (prec, sub) = parse_width(next_char(sub), args);
+            (Some(prec), sub)
+        }
+        _ => (None, sub),
+    }
+}
+
+#[derive(Debug, Copy, Clone)]
+enum Length {
+    Int,
+    /// `hh`
+    Char,
+    /// `h`
+    Short,
+    /// `l`
+    Long,
+    /// `ll`
+    LongLong,
+    /// `z`
+    Usize,
+    /// `t`
+    Isize,
+}
+
+impl Length {
+    unsafe fn parse_signed(self, args: &mut VaList) -> SignedInt {
+        match self {
+            Length::Int => SignedInt::Int(args.arg()),
+            Length::Char => SignedInt::Char(args.arg()),
+            Length::Short => SignedInt::Short(args.arg()),
+            Length::Long => SignedInt::Long(args.arg()),
+            Length::LongLong => SignedInt::LongLong(args.arg()),
+            // for some reason, these exist as different options, yet produce the same output
+            Length::Usize | Length::Isize => SignedInt::Isize(args.arg()),
+        }
+    }
+    unsafe fn parse_unsigned(self, args: &mut VaList) -> UnsignedInt {
+        match self {
+            Length::Int => UnsignedInt::Int(args.arg()),
+            Length::Char => UnsignedInt::Char(args.arg()),
+            Length::Short => UnsignedInt::Short(args.arg()),
+            Length::Long => UnsignedInt::Long(args.arg()),
+            Length::LongLong => UnsignedInt::LongLong(args.arg()),
+            // for some reason, these exist as different options, yet produce the same output
+            Length::Usize | Length::Isize => UnsignedInt::Isize(args.arg()),
+        }
+    }
+}
+
+/// Parse the [Length field](https://en.wikipedia.org/wiki/Printf_format_string#Length_field).
+fn parse_length(sub: &[u8]) -> (Length, &[u8]) {
+    match sub.get(0).copied() {
+        Some(b'h') => match sub.get(1).copied() {
+            Some(b'h') => (Length::Char, sub.get(2..).unwrap_or(&[])),
+            _ => (Length::Short, next_char(sub)),
+        },
+        Some(b'l') => match sub.get(1).copied() {
+            Some(b'l') => (Length::LongLong, sub.get(2..).unwrap_or(&[])),
+            _ => (Length::Long, next_char(sub)),
+        },
+        Some(b'z') => (Length::Usize, next_char(sub)),
+        Some(b't') => (Length::Isize, next_char(sub)),
+        _ => (Length::Int, sub),
+    }
+}
+
+/// Parse a format parameter and write it somewhere.
+///
+/// # Safety
+///
+/// [`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/).
+pub unsafe fn format(
+    format: *const c_char,
+    mut args: VaList,
+    mut handler: impl FnMut(Argument) -> c_int,
+) -> c_int {
+    let str = CStr::from_ptr(format).to_bytes();
+    let mut iter = str.split(|&c| c == b'%');
+    let mut written = 0;
+
+    macro_rules! err {
+        ($ex: expr) => {{
+            let res = $ex;
+            if res < 0 {
+                return -1;
+            } else {
+                written += res;
+            }
+        }};
+    }
+    if let Some(begin) = iter.next() {
+        err!(handler(Specifier::Bytes(begin).into()));
+    }
+    let mut last_was_percent = false;
+    for (sub, next) in iter.map(Some).chain(core::iter::once(None)).tuple_windows() {
+        let sub = match sub {
+            Some(sub) => sub,
+            None => break,
+        };
+        if last_was_percent {
+            err!(handler(Specifier::Bytes(sub).into()));
+            last_was_percent = false;
+            continue;
+        }
+        let (flags, sub) = parse_flags(sub);
+        let (width, sub) = parse_width(sub, &mut args);
+        let (precision, sub) = parse_precision(sub, &mut args);
+        let (length, sub) = parse_length(sub);
+        let ch = sub
+            .get(0)
+            .unwrap_or(if next.is_some() { &b'%' } else { &0 });
+        err!(handler(Argument {
+            flags,
+            width,
+            precision,
+            specifier: match ch {
+                b'%' => {
+                    last_was_percent = true;
+                    Specifier::Percent
+                }
+                b'd' | b'i' => Specifier::Int(length.parse_signed(&mut args)),
+                b'x' => Specifier::Hex(length.parse_unsigned(&mut args)),
+                b'X' => Specifier::UpperHex(length.parse_unsigned(&mut args)),
+                b'u' => Specifier::Uint(length.parse_unsigned(&mut args)),
+                b'o' => Specifier::Octal(length.parse_unsigned(&mut args)),
+                b'f' | b'F' => Specifier::Double {
+                    value: args.arg(),
+                    format: DoubleFormat::Normal.set_upper(ch.is_ascii_uppercase()),
+                },
+                b'e' | b'E' => Specifier::Double {
+                    value: args.arg(),
+                    format: DoubleFormat::Scientific.set_upper(ch.is_ascii_uppercase()),
+                },
+                b'g' | b'G' => Specifier::Double {
+                    value: args.arg(),
+                    format: DoubleFormat::Auto.set_upper(ch.is_ascii_uppercase()),
+                },
+                b'a' | b'A' => Specifier::Double {
+                    value: args.arg(),
+                    format: DoubleFormat::Hex.set_upper(ch.is_ascii_uppercase()),
+                },
+                b's' => Specifier::String(CStr::from_ptr(args.arg())),
+                b'c' => Specifier::Char(args.arg()),
+                b'p' => Specifier::Pointer(args.arg()),
+                b'n' => Specifier::WriteBytesWritten(written, args.arg()),
+                _ => return -1,
+            },
+        }));
+        err!(handler(Specifier::Bytes(next_char(sub)).into()));
+    }
+    written
+}

+ 196 - 0
src/tests.rs

@@ -0,0 +1,196 @@
+use core::ptr::null_mut;
+use cty::*;
+
+extern "C" {
+    fn asprintf(s: *mut *mut u8, format: *const u8, ...) -> c_int;
+    fn free(p: *mut c_void);
+}
+
+unsafe extern "C" fn rust_fmt(str: *const u8, mut args: ...) -> Box<String> {
+    let mut s = String::new();
+    assert!(
+        crate::format(
+            str as _,
+            args.clone().as_va_list(),
+            crate::output::fmt_write(&mut s),
+        ) >= 0
+    );
+    let mut s2 = std::io::Cursor::new(vec![]);
+    assert!(
+        crate::format(
+            str as _,
+            args.as_va_list(),
+            crate::output::io_write(&mut s2),
+        ) >= 0
+    );
+    assert_eq!(s.as_bytes(), s2.get_ref());
+    Box::new(s)
+}
+
+macro_rules! c_fmt {
+    ($format:expr $(, $p:expr)*) => {{
+        let mut ptr = null_mut();
+        assert!(asprintf(&mut ptr, $format $(, $p)*) >= 0);
+        let str: String = cstr_core::CStr::from_ptr(ptr as *const _).to_string_lossy().into();
+        free(ptr as _);
+        str
+    }};
+}
+
+macro_rules! assert_eq_fmt {
+    ($format:expr $(, $p:expr)*) => {
+        assert_eq!(c_fmt!($format $(, $p)*).as_str(), rust_fmt($format, $($p),*).as_str())
+    };
+}
+
+macro_rules! c_str {
+    ($data:literal) => {
+        concat!($data, "\0").as_ptr()
+    };
+}
+
+#[test]
+fn test_plain() {
+    unsafe {
+        assert_eq_fmt!(c_str!("abc"));
+        assert_eq_fmt!(c_str!(""));
+        assert_eq_fmt!(c_str!("%%"));
+        assert_eq_fmt!(c_str!("%% def"));
+        assert_eq_fmt!(c_str!("abc %%"));
+        assert_eq_fmt!(c_str!("abc %% def"));
+        assert_eq_fmt!(c_str!("abc %%%% def"));
+        assert_eq_fmt!(c_str!("%%%%%%"));
+    }
+}
+
+#[test]
+fn test_str() {
+    unsafe {
+        assert_eq_fmt!(c_str!("hello %s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("hello %%%s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("%10s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("%.4s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("%10.4s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("%-10.4s"), c_str!("world"));
+        assert_eq_fmt!(c_str!("%-10s"), c_str!("world"));
+    }
+}
+
+#[test]
+fn test_int() {
+    unsafe {
+        assert_eq_fmt!(c_str!("% 0*i"), 23125, 17);
+        assert_eq_fmt!(c_str!("% 010i"), 23125);
+        assert_eq_fmt!(c_str!("% 10i"), 23125);
+        assert_eq_fmt!(c_str!("% 5i"), 23125);
+        assert_eq_fmt!(c_str!("% 4i"), 23125);
+        assert_eq_fmt!(c_str!("%- 010i"), 23125);
+        assert_eq_fmt!(c_str!("%- 10i"), 23125);
+        assert_eq_fmt!(c_str!("%- 5i"), 23125);
+        assert_eq_fmt!(c_str!("%- 4i"), 23125);
+        assert_eq_fmt!(c_str!("%+ 010i"), 23125);
+        assert_eq_fmt!(c_str!("%+ 10i"), 23125);
+        assert_eq_fmt!(c_str!("%+ 5i"), 23125);
+        assert_eq_fmt!(c_str!("%+ 4i"), 23125);
+        assert_eq_fmt!(c_str!("%-010i"), 23125);
+        assert_eq_fmt!(c_str!("%-10i"), 23125);
+        assert_eq_fmt!(c_str!("%-5i"), 23125);
+        assert_eq_fmt!(c_str!("%-4i"), 23125);
+    }
+}
+
+#[test]
+fn test_octal() {
+    unsafe {
+        assert_eq_fmt!(c_str!("% 010o"), 23125);
+        assert_eq_fmt!(c_str!("% 10o"), 23125);
+        assert_eq_fmt!(c_str!("% 5o"), 23125);
+        assert_eq_fmt!(c_str!("% 4o"), 23125);
+        assert_eq_fmt!(c_str!("%- 010o"), 23125);
+        assert_eq_fmt!(c_str!("%- 10o"), 23125);
+        assert_eq_fmt!(c_str!("%- 5o"), 23125);
+        assert_eq_fmt!(c_str!("%- 4o"), 23125);
+        assert_eq_fmt!(c_str!("%+ 010o"), 23125);
+        assert_eq_fmt!(c_str!("%+ 10o"), 23125);
+        assert_eq_fmt!(c_str!("%+ 5o"), 23125);
+        assert_eq_fmt!(c_str!("%+ 4o"), 23125);
+        assert_eq_fmt!(c_str!("%-010o"), 23125);
+        assert_eq_fmt!(c_str!("%-10o"), 23125);
+        assert_eq_fmt!(c_str!("%-5o"), 23125);
+        assert_eq_fmt!(c_str!("%-4o"), 23125);
+    }
+}
+
+#[test]
+fn test_hex() {
+    unsafe {
+        assert_eq_fmt!(c_str!("% 010x"), 23125);
+        assert_eq_fmt!(c_str!("% 10x"), 23125);
+        assert_eq_fmt!(c_str!("% 5x"), 23125);
+        assert_eq_fmt!(c_str!("% 4x"), 23125);
+        assert_eq_fmt!(c_str!("%- 010x"), 23125);
+        assert_eq_fmt!(c_str!("%- 10x"), 23125);
+        assert_eq_fmt!(c_str!("%- 5x"), 23125);
+        assert_eq_fmt!(c_str!("%- 4x"), 23125);
+        assert_eq_fmt!(c_str!("%+ 010x"), 23125);
+        assert_eq_fmt!(c_str!("%+ 10x"), 23125);
+        assert_eq_fmt!(c_str!("%+ 5x"), 23125);
+        assert_eq_fmt!(c_str!("%+ 4x"), 23125);
+        assert_eq_fmt!(c_str!("%-010x"), 23125);
+        assert_eq_fmt!(c_str!("%-10x"), 23125);
+        assert_eq_fmt!(c_str!("%-5x"), 23125);
+        assert_eq_fmt!(c_str!("%-4x"), 23125);
+
+        assert_eq_fmt!(c_str!("%# 010x"), 23125);
+        assert_eq_fmt!(c_str!("%# 10x"), 23125);
+        assert_eq_fmt!(c_str!("%# 5x"), 23125);
+        assert_eq_fmt!(c_str!("%# 4x"), 23125);
+        assert_eq_fmt!(c_str!("%#- 010x"), 23125);
+        assert_eq_fmt!(c_str!("%#- 10x"), 23125);
+        assert_eq_fmt!(c_str!("%#- 5x"), 23125);
+        assert_eq_fmt!(c_str!("%#- 4x"), 23125);
+        assert_eq_fmt!(c_str!("%#+ 010x"), 23125);
+        assert_eq_fmt!(c_str!("%#+ 10x"), 23125);
+        assert_eq_fmt!(c_str!("%#+ 5x"), 23125);
+        assert_eq_fmt!(c_str!("%#+ 4x"), 23125);
+        assert_eq_fmt!(c_str!("%#-010x"), 23125);
+        assert_eq_fmt!(c_str!("%#-10x"), 23125);
+        assert_eq_fmt!(c_str!("%#-5x"), 23125);
+        assert_eq_fmt!(c_str!("%#-4x"), 23125);
+
+        assert_eq_fmt!(c_str!("% 010X"), 23125);
+        assert_eq_fmt!(c_str!("% 10X"), 23125);
+        assert_eq_fmt!(c_str!("% 5X"), 23125);
+        assert_eq_fmt!(c_str!("% 4X"), 23125);
+        assert_eq_fmt!(c_str!("%- 010X"), 23125);
+        assert_eq_fmt!(c_str!("%- 10X"), 23125);
+        assert_eq_fmt!(c_str!("%- 5X"), 23125);
+        assert_eq_fmt!(c_str!("%- 4X"), 23125);
+        assert_eq_fmt!(c_str!("%+ 010X"), 23125);
+        assert_eq_fmt!(c_str!("%+ 10X"), 23125);
+        assert_eq_fmt!(c_str!("%+ 5X"), 23125);
+        assert_eq_fmt!(c_str!("%+ 4X"), 23125);
+        assert_eq_fmt!(c_str!("%-010X"), 23125);
+        assert_eq_fmt!(c_str!("%-10X"), 23125);
+        assert_eq_fmt!(c_str!("%-5X"), 23125);
+        assert_eq_fmt!(c_str!("%-4X"), 23125);
+    }
+}
+
+#[test]
+fn test_float() {
+    unsafe {
+        assert_eq_fmt!(c_str!("%f"), 1234f64);
+        assert_eq_fmt!(c_str!("%.5f"), 1234f64);
+        assert_eq_fmt!(c_str!("%.*f"), 1234f64, 3);
+    }
+}
+
+#[test]
+fn test_char() {
+    unsafe {
+        assert_eq_fmt!(c_str!("%c"), b'a' as c_int);
+        assert_eq_fmt!(c_str!("%10c"), b'a' as c_int);
+        assert_eq_fmt!(c_str!("%-10c"), b'a' as c_int);
+    }
+}