Import uniquote 3.3.0 upstream upstream/3.3.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 8 Mar 2023 03:48:06 +0000 (12:48 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 8 Mar 2023 03:48:06 +0000 (12:48 +0900)
15 files changed:
.cargo_vcs_info.json [new file with mode: 0644]
COPYRIGHT [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE-APACHE [new file with mode: 0644]
LICENSE-MIT [new file with mode: 0644]
LICENSE-THIRD-PARTY [new file with mode: 0644]
README.md [new file with mode: 0644]
src/escape/code_point.rs [new file with mode: 0644]
src/escape/mod.rs [new file with mode: 0644]
src/escape/tables/mod.rs [new file with mode: 0644]
src/escape/tables/unprintable.rs [new file with mode: 0644]
src/formatter.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/quote.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..6ddae11
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "5939ce196b25265dcc596de06cadc94701b939a8"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644 (file)
index 0000000..27c9bfb
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1,5 @@
+Copyright (c) 2020 dylni (https://github.com/dylni)
+
+Licensed under the Apache License, Version 2.0 <LICENSE-APACHE> or the MIT
+license <LICENSE-MIT>, at your option. All files in this project may not be
+copied, modified, or distributed except according to those terms.
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..8151c63
--- /dev/null
@@ -0,0 +1,51 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.64.0"
+name = "uniquote"
+version = "3.3.0"
+authors = ["dylni"]
+exclude = [
+    ".*",
+    "tests.rs",
+    "/rustfmt.toml",
+    "/src/bin",
+    "/tests",
+]
+description = """
+Quote strings for clear display in output
+"""
+readme = "README.md"
+keywords = [
+    "osstr",
+    "path",
+    "print",
+    "quote",
+    "unprintable",
+]
+categories = [
+    "command-line-interface",
+    "no-std",
+    "value-formatting",
+    "wasm",
+]
+license = "(MIT OR Apache-2.0) AND Unicode-DFS-2016"
+repository = "https://github.com/dylni/uniquote"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[features]
+alloc = []
+default = ["std"]
+std = ["alloc"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..e588f78
--- /dev/null
@@ -0,0 +1,24 @@
+[package]
+name = "uniquote"
+version = "3.3.0"
+authors = ["dylni"]
+edition = "2021"
+rust-version = "1.64.0"
+description = """
+Quote strings for clear display in output
+"""
+readme = "README.md"
+repository = "https://github.com/dylni/uniquote"
+license = "(MIT OR Apache-2.0) AND Unicode-DFS-2016"
+keywords = ["osstr", "path", "print", "quote", "unprintable"]
+categories = ["command-line-interface", "no-std", "value-formatting", "wasm"]
+exclude = [".*", "tests.rs", "/rustfmt.toml", "/src/bin", "/tests"]
+
+[package.metadata.docs.rs]
+all-features = true
+
+[features]
+default = ["std"]
+
+alloc = []
+std = ["alloc"]
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644 (file)
index 0000000..d9a10c0
--- /dev/null
@@ -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
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644 (file)
index 0000000..5a602c0
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 dylni (https://github.com/dylni)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY
new file mode 100644 (file)
index 0000000..c8b7100
--- /dev/null
@@ -0,0 +1,51 @@
+===============================================================================
+
+Unicode, Inc.
+https://www.unicode.org/license.txt
+
+UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
+
+See Terms of Use <https://www.unicode.org/copyright.html>
+for definitions of Unicode Inc.’s Data Files and Software.
+
+NOTICE TO USER: Carefully read the following legal agreement.
+BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S
+DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"),
+YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT.
+IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE
+THE DATA FILES OR SOFTWARE.
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 1991-2022 Unicode, Inc. All rights reserved.
+Distributed under the Terms of Use in https://www.unicode.org/copyright.html.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation
+(the "Data Files") or Unicode software and any associated documentation
+(the "Software") to deal in the Data Files or Software
+without restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, and/or sell copies of
+the Data Files or Software, and to permit persons to whom the Data Files
+or Software are furnished to do so, provided that either
+(a) this copyright and permission notice appear with all copies
+of the Data Files or Software, or
+(b) this copyright and permission notice appear in associated
+Documentation.
+
+THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
+NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale,
+use or other dealings in these Data Files or Software without prior
+written authorization of the copyright holder.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..44f84d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+# UniQuote
+
+This crate allows quoting strings for use in output. It works similarly to
+[`str::escape_debug`], but the result is meant to be shown to users. Simply
+call [`Quote::quote`] on an argument passed to [`println!`] or a similar macro
+to quote it.
+
+One of the primary uses for this crate is displaying paths losslessly. Since
+[`Path`] has no [`Display`] implementation, it is usually output by calling
+[`Path::display`] or [`Path::to_string_lossy`] beforehand. However, both of
+those methods are lossy; they replace all invalid characters with
+[`REPLACEMENT_CHARACTER`]. This crate escapes those invalid characters instead,
+allowing them to always be displayed correctly.
+
+[![GitHub Build Status](https://github.com/dylni/uniquote/workflows/build/badge.svg?branch=master)](https://github.com/dylni/uniquote/actions?query=branch%3Amaster)
+
+## Usage
+
+Add the following lines to your "Cargo.toml" file:
+
+```toml
+[dependencies]
+uniquote = "3.3"
+```
+
+See the [documentation] for available functionality and examples.
+
+## Rust version support
+
+The minimum supported Rust toolchain version is currently Rust 1.64.0.
+
+Minor version updates may increase this version requirement. However, the
+previous two Rust releases will always be supported. If the minimum Rust
+version must not be increased, use a tilde requirement to prevent updating this
+crate's minor version:
+
+```toml
+[dependencies]
+uniquote = "~3.3"
+```
+
+## License
+
+Licensing terms are specified in [COPYRIGHT].
+
+Unless you explicitly state otherwise, any contribution submitted for inclusion
+in this crate, as defined in [LICENSE-APACHE], shall be licensed according to
+[COPYRIGHT], without any additional terms or conditions.
+
+### Third-party content
+
+This crate includes copies and modifications of content developed by third
+parties:
+
+- [src/escape/tables/unprintable.rs] contains structured data defined by
+  Unicode, Inc., licensed under the Unicode License.
+
+See that file for more details.
+
+Copies of third-party licenses can be found in [LICENSE-THIRD-PARTY].
+
+[COPYRIGHT]: https://github.com/dylni/uniquote/blob/master/COPYRIGHT
+[`Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html
+[documentation]: https://docs.rs/uniquote
+[LICENSE-APACHE]: https://github.com/dylni/uniquote/blob/master/LICENSE-APACHE
+[LICENSE-THIRD-PARTY]: https://github.com/dylni/uniquote/blob/master/LICENSE-THIRD-PARTY
+[`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
+[`Path::display`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.display
+[`Path::to_string_lossy`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.to_string_lossy
+[`println!`]: https://doc.rust-lang.org/std/macro.println.html
+[`Quote::quote`]: https://docs.rs/uniquote/*/uniquote/trait.Quote.html#method.quote
+[`REPLACEMENT_CHARACTER`]: https://doc.rust-lang.org/std/char/constant.REPLACEMENT_CHARACTER.html
+[src/escape/tables/unprintable.rs]: https://github.com/dylni/uniquote/blob/master/src/escape/tables/unprintable.rs
+[`str::escape_debug`]: https://doc.rust-lang.org/std/primitive.str.html#method.escape_debug
diff --git a/src/escape/code_point.rs b/src/escape/code_point.rs
new file mode 100644 (file)
index 0000000..56aee14
--- /dev/null
@@ -0,0 +1,33 @@
+use core::char::CharTryFromError;
+use core::char::DecodeUtf16Error;
+use core::convert::TryFrom;
+use core::convert::TryInto;
+
+#[derive(Clone, Copy)]
+pub(super) struct CodePoint(u32);
+
+impl From<char> for CodePoint {
+    fn from(value: char) -> Self {
+        Self(value.into())
+    }
+}
+
+impl From<DecodeUtf16Error> for CodePoint {
+    fn from(value: DecodeUtf16Error) -> Self {
+        Self(value.unpaired_surrogate().into())
+    }
+}
+
+impl From<CodePoint> for u32 {
+    fn from(value: CodePoint) -> Self {
+        value.0
+    }
+}
+
+impl TryFrom<CodePoint> for char {
+    type Error = CharTryFromError;
+
+    fn try_from(value: CodePoint) -> Result<Self, Self::Error> {
+        value.0.try_into()
+    }
+}
diff --git a/src/escape/mod.rs b/src/escape/mod.rs
new file mode 100644 (file)
index 0000000..45cc093
--- /dev/null
@@ -0,0 +1,195 @@
+use core::char;
+use core::convert::TryFrom;
+use core::fmt;
+use core::fmt::Formatter;
+use core::fmt::Write as _;
+use core::str;
+
+use super::END_ESCAPE;
+use super::QUOTE;
+use super::START_ESCAPE;
+
+mod code_point;
+use code_point::CodePoint;
+
+mod tables;
+use tables::UNPRINTABLE;
+
+fn table_contains(table: &[(u32, u32)], code_point: CodePoint) -> bool {
+    let code_point = code_point.into();
+    table
+        .binary_search_by_key(&code_point, |&(x, _)| x)
+        .err()
+        .map(|index| {
+            index
+                .checked_sub(1)
+                .map(|x| code_point <= table[x].1)
+                .unwrap_or(false)
+        })
+        .unwrap_or(true)
+}
+
+fn is_printable(ch: char) -> bool {
+    // ASCII is very common, so it should be optimized.
+    (' '..='~').contains(&ch)
+        || (!ch.is_ascii() && !table_contains(UNPRINTABLE, ch.into()))
+}
+
+enum EscapedCodePoint {
+    Hex(CodePoint),
+    Literal(char),
+    Quote(),
+    Repeated(char),
+    Sequence(&'static str),
+}
+
+impl EscapedCodePoint {
+    fn format(self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Literal(ch) => return f.write_char(ch),
+            Self::Repeated(ch) => {
+                for _ in 0..2 {
+                    f.write_char(ch)?;
+                }
+                return Ok(());
+            }
+            _ => {}
+        }
+
+        f.write_char(START_ESCAPE)?;
+
+        if matches!(self, Self::Quote()) {
+            f.write_char(QUOTE)?;
+        } else {
+            f.write_char('~')?;
+
+            match self {
+                Self::Hex(code_point) => {
+                    write!(f, "u{:x}", u32::from(code_point))?;
+                }
+                Self::Sequence(sequence) => f.write_str(sequence)?,
+                _ => unreachable!(),
+            }
+        }
+
+        f.write_char(END_ESCAPE)
+    }
+}
+
+impl From<u8> for EscapedCodePoint {
+    fn from(value: u8) -> Self {
+        char::from(value).into()
+    }
+}
+
+impl From<char> for EscapedCodePoint {
+    fn from(value: char) -> Self {
+        match value {
+            '\t' => Self::Sequence("t"),
+            '\n' => Self::Sequence("n"),
+            '\r' => Self::Sequence("r"),
+
+            QUOTE => Self::Quote(),
+            END_ESCAPE | START_ESCAPE => Self::Repeated(value),
+
+            _ if is_printable(value) => Self::Literal(value),
+            _ => Self::Hex(value.into()),
+        }
+    }
+}
+
+impl From<CodePoint> for EscapedCodePoint {
+    fn from(value: CodePoint) -> Self {
+        // Upon error, [value] is known to be a surrogate, so it is
+        // unprintable.
+        char::try_from(value)
+            .map(Into::into)
+            .unwrap_or(Self::Hex(value))
+    }
+}
+
+pub(super) trait Escape {
+    fn escape(&self, f: &mut Formatter<'_>) -> fmt::Result;
+}
+
+impl Escape for char {
+    fn escape(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        self.encode_utf8(&mut [0; 4]).escape(f)
+    }
+}
+
+impl Escape for str {
+    fn escape(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        // [str] can be written more efficiently than multiple [char] values,
+        // since it is already encoded as UTF-8 bytes. The [Debug]
+        // implementation for [str] uses the same optimization.
+        let mut escaped_index = 0;
+        macro_rules! push_literal {
+            ( $index:expr ) => {
+                let index = $index;
+                if index != escaped_index {
+                    f.write_str(&self[escaped_index..index])?;
+                }
+            };
+        }
+
+        let mut escaped = false;
+        for (i, ch) in self.char_indices() {
+            if escaped {
+                escaped_index = i;
+            }
+
+            let code_point = ch.into();
+            escaped = !matches!(code_point, EscapedCodePoint::Literal(_));
+            if escaped {
+                push_literal!(i);
+                code_point.format(f)?;
+            }
+        }
+        if !escaped {
+            push_literal!(self.len());
+        }
+
+        Ok(())
+    }
+}
+
+impl Escape for [u8] {
+    fn escape(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let mut string = self;
+        while !string.is_empty() {
+            let mut invalid = &b""[..];
+            let valid = str::from_utf8(string).unwrap_or_else(|error| {
+                let (valid, string) = string.split_at(error.valid_up_to());
+
+                let invalid_length =
+                    error.error_len().unwrap_or_else(|| string.len());
+                invalid = &string[..invalid_length];
+
+                // SAFETY: This slice was validated to be UTF-8.
+                unsafe { str::from_utf8_unchecked(valid) }
+            });
+
+            valid.escape(f)?;
+            string = &string[valid.len()..];
+
+            for &byte in invalid {
+                EscapedCodePoint::from(byte).format(f)?;
+            }
+            string = &string[invalid.len()..];
+        }
+        Ok(())
+    }
+}
+
+pub(super) fn escape_utf16<I>(iter: I, f: &mut Formatter<'_>) -> fmt::Result
+where
+    I: IntoIterator<Item = u16>,
+{
+    for ch in char::decode_utf16(iter) {
+        ch.map(EscapedCodePoint::from)
+            .unwrap_or_else(|x| CodePoint::from(x).into())
+            .format(f)?;
+    }
+    Ok(())
+}
diff --git a/src/escape/tables/mod.rs b/src/escape/tables/mod.rs
new file mode 100644 (file)
index 0000000..a1a4fed
--- /dev/null
@@ -0,0 +1,5 @@
+#![allow(clippy::redundant_static_lifetimes)]
+
+#[rustfmt::skip]
+mod unprintable;
+pub(super) use unprintable::UNPRINTABLE;
diff --git a/src/escape/tables/unprintable.rs b/src/escape/tables/unprintable.rs
new file mode 100644 (file)
index 0000000..261e1d0
--- /dev/null
@@ -0,0 +1,175 @@
+// DO NOT EDIT THIS FILE. IT WAS AUTOMATICALLY GENERATED BY:
+//
+//   ucd-generate general-category ucd-15.0.0 --combined --include c,z --name UNPRINTABLE
+//
+// Unicode version: 15.0.0.
+//
+// ucd-generate 0.2.13 is available on crates.io.
+
+pub const UNPRINTABLE: &'static [(u32, u32)] = &[
+  (0, 32), (127, 160), (173, 173), (888, 889), (896, 899), (907, 907),
+  (909, 909), (930, 930), (1328, 1328), (1367, 1368), (1419, 1420),
+  (1424, 1424), (1480, 1487), (1515, 1518), (1525, 1541), (1564, 1564),
+  (1757, 1757), (1806, 1807), (1867, 1868), (1970, 1983), (2043, 2044),
+  (2094, 2095), (2111, 2111), (2140, 2141), (2143, 2143), (2155, 2159),
+  (2191, 2199), (2274, 2274), (2436, 2436), (2445, 2446), (2449, 2450),
+  (2473, 2473), (2481, 2481), (2483, 2485), (2490, 2491), (2501, 2502),
+  (2505, 2506), (2511, 2518), (2520, 2523), (2526, 2526), (2532, 2533),
+  (2559, 2560), (2564, 2564), (2571, 2574), (2577, 2578), (2601, 2601),
+  (2609, 2609), (2612, 2612), (2615, 2615), (2618, 2619), (2621, 2621),
+  (2627, 2630), (2633, 2634), (2638, 2640), (2642, 2648), (2653, 2653),
+  (2655, 2661), (2679, 2688), (2692, 2692), (2702, 2702), (2706, 2706),
+  (2729, 2729), (2737, 2737), (2740, 2740), (2746, 2747), (2758, 2758),
+  (2762, 2762), (2766, 2767), (2769, 2783), (2788, 2789), (2802, 2808),
+  (2816, 2816), (2820, 2820), (2829, 2830), (2833, 2834), (2857, 2857),
+  (2865, 2865), (2868, 2868), (2874, 2875), (2885, 2886), (2889, 2890),
+  (2894, 2900), (2904, 2907), (2910, 2910), (2916, 2917), (2936, 2945),
+  (2948, 2948), (2955, 2957), (2961, 2961), (2966, 2968), (2971, 2971),
+  (2973, 2973), (2976, 2978), (2981, 2983), (2987, 2989), (3002, 3005),
+  (3011, 3013), (3017, 3017), (3022, 3023), (3025, 3030), (3032, 3045),
+  (3067, 3071), (3085, 3085), (3089, 3089), (3113, 3113), (3130, 3131),
+  (3141, 3141), (3145, 3145), (3150, 3156), (3159, 3159), (3163, 3164),
+  (3166, 3167), (3172, 3173), (3184, 3190), (3213, 3213), (3217, 3217),
+  (3241, 3241), (3252, 3252), (3258, 3259), (3269, 3269), (3273, 3273),
+  (3278, 3284), (3287, 3292), (3295, 3295), (3300, 3301), (3312, 3312),
+  (3316, 3327), (3341, 3341), (3345, 3345), (3397, 3397), (3401, 3401),
+  (3408, 3411), (3428, 3429), (3456, 3456), (3460, 3460), (3479, 3481),
+  (3506, 3506), (3516, 3516), (3518, 3519), (3527, 3529), (3531, 3534),
+  (3541, 3541), (3543, 3543), (3552, 3557), (3568, 3569), (3573, 3584),
+  (3643, 3646), (3676, 3712), (3715, 3715), (3717, 3717), (3723, 3723),
+  (3748, 3748), (3750, 3750), (3774, 3775), (3781, 3781), (3783, 3783),
+  (3791, 3791), (3802, 3803), (3808, 3839), (3912, 3912), (3949, 3952),
+  (3992, 3992), (4029, 4029), (4045, 4045), (4059, 4095), (4294, 4294),
+  (4296, 4300), (4302, 4303), (4681, 4681), (4686, 4687), (4695, 4695),
+  (4697, 4697), (4702, 4703), (4745, 4745), (4750, 4751), (4785, 4785),
+  (4790, 4791), (4799, 4799), (4801, 4801), (4806, 4807), (4823, 4823),
+  (4881, 4881), (4886, 4887), (4955, 4956), (4989, 4991), (5018, 5023),
+  (5110, 5111), (5118, 5119), (5760, 5760), (5789, 5791), (5881, 5887),
+  (5910, 5918), (5943, 5951), (5972, 5983), (5997, 5997), (6001, 6001),
+  (6004, 6015), (6110, 6111), (6122, 6127), (6138, 6143), (6158, 6158),
+  (6170, 6175), (6265, 6271), (6315, 6319), (6390, 6399), (6431, 6431),
+  (6444, 6447), (6460, 6463), (6465, 6467), (6510, 6511), (6517, 6527),
+  (6572, 6575), (6602, 6607), (6619, 6621), (6684, 6685), (6751, 6751),
+  (6781, 6782), (6794, 6799), (6810, 6815), (6830, 6831), (6863, 6911),
+  (6989, 6991), (7039, 7039), (7156, 7163), (7224, 7226), (7242, 7244),
+  (7305, 7311), (7355, 7356), (7368, 7375), (7419, 7423), (7958, 7959),
+  (7966, 7967), (8006, 8007), (8014, 8015), (8024, 8024), (8026, 8026),
+  (8028, 8028), (8030, 8030), (8062, 8063), (8117, 8117), (8133, 8133),
+  (8148, 8149), (8156, 8156), (8176, 8177), (8181, 8181), (8191, 8207),
+  (8232, 8239), (8287, 8303), (8306, 8307), (8335, 8335), (8349, 8351),
+  (8385, 8399), (8433, 8447), (8588, 8591), (9255, 9279), (9291, 9311),
+  (11124, 11125), (11158, 11158), (11508, 11512), (11558, 11558),
+  (11560, 11564), (11566, 11567), (11624, 11630), (11633, 11646),
+  (11671, 11679), (11687, 11687), (11695, 11695), (11703, 11703),
+  (11711, 11711), (11719, 11719), (11727, 11727), (11735, 11735),
+  (11743, 11743), (11870, 11903), (11930, 11930), (12020, 12031),
+  (12246, 12271), (12284, 12288), (12352, 12352), (12439, 12440),
+  (12544, 12548), (12592, 12592), (12687, 12687), (12772, 12783),
+  (12831, 12831), (42125, 42127), (42183, 42191), (42540, 42559),
+  (42744, 42751), (42955, 42959), (42962, 42962), (42964, 42964),
+  (42970, 42993), (43053, 43055), (43066, 43071), (43128, 43135),
+  (43206, 43213), (43226, 43231), (43348, 43358), (43389, 43391),
+  (43470, 43470), (43482, 43485), (43519, 43519), (43575, 43583),
+  (43598, 43599), (43610, 43611), (43715, 43738), (43767, 43776),
+  (43783, 43784), (43791, 43792), (43799, 43807), (43815, 43815),
+  (43823, 43823), (43884, 43887), (44014, 44015), (44026, 44031),
+  (55204, 55215), (55239, 55242), (55292, 63743), (64110, 64111),
+  (64218, 64255), (64263, 64274), (64280, 64284), (64311, 64311),
+  (64317, 64317), (64319, 64319), (64322, 64322), (64325, 64325),
+  (64451, 64466), (64912, 64913), (64968, 64974), (64976, 65007),
+  (65050, 65055), (65107, 65107), (65127, 65127), (65132, 65135),
+  (65141, 65141), (65277, 65280), (65471, 65473), (65480, 65481),
+  (65488, 65489), (65496, 65497), (65501, 65503), (65511, 65511),
+  (65519, 65531), (65534, 65535), (65548, 65548), (65575, 65575),
+  (65595, 65595), (65598, 65598), (65614, 65615), (65630, 65663),
+  (65787, 65791), (65795, 65798), (65844, 65846), (65935, 65935),
+  (65949, 65951), (65953, 65999), (66046, 66175), (66205, 66207),
+  (66257, 66271), (66300, 66303), (66340, 66348), (66379, 66383),
+  (66427, 66431), (66462, 66462), (66500, 66503), (66518, 66559),
+  (66718, 66719), (66730, 66735), (66772, 66775), (66812, 66815),
+  (66856, 66863), (66916, 66926), (66939, 66939), (66955, 66955),
+  (66963, 66963), (66966, 66966), (66978, 66978), (66994, 66994),
+  (67002, 67002), (67005, 67071), (67383, 67391), (67414, 67423),
+  (67432, 67455), (67462, 67462), (67505, 67505), (67515, 67583),
+  (67590, 67591), (67593, 67593), (67638, 67638), (67641, 67643),
+  (67645, 67646), (67670, 67670), (67743, 67750), (67760, 67807),
+  (67827, 67827), (67830, 67834), (67868, 67870), (67898, 67902),
+  (67904, 67967), (68024, 68027), (68048, 68049), (68100, 68100),
+  (68103, 68107), (68116, 68116), (68120, 68120), (68150, 68151),
+  (68155, 68158), (68169, 68175), (68185, 68191), (68256, 68287),
+  (68327, 68330), (68343, 68351), (68406, 68408), (68438, 68439),
+  (68467, 68471), (68498, 68504), (68509, 68520), (68528, 68607),
+  (68681, 68735), (68787, 68799), (68851, 68857), (68904, 68911),
+  (68922, 69215), (69247, 69247), (69290, 69290), (69294, 69295),
+  (69298, 69372), (69416, 69423), (69466, 69487), (69514, 69551),
+  (69580, 69599), (69623, 69631), (69710, 69713), (69750, 69758),
+  (69821, 69821), (69827, 69839), (69865, 69871), (69882, 69887),
+  (69941, 69941), (69960, 69967), (70007, 70015), (70112, 70112),
+  (70133, 70143), (70162, 70162), (70210, 70271), (70279, 70279),
+  (70281, 70281), (70286, 70286), (70302, 70302), (70314, 70319),
+  (70379, 70383), (70394, 70399), (70404, 70404), (70413, 70414),
+  (70417, 70418), (70441, 70441), (70449, 70449), (70452, 70452),
+  (70458, 70458), (70469, 70470), (70473, 70474), (70478, 70479),
+  (70481, 70486), (70488, 70492), (70500, 70501), (70509, 70511),
+  (70517, 70655), (70748, 70748), (70754, 70783), (70856, 70863),
+  (70874, 71039), (71094, 71095), (71134, 71167), (71237, 71247),
+  (71258, 71263), (71277, 71295), (71354, 71359), (71370, 71423),
+  (71451, 71452), (71468, 71471), (71495, 71679), (71740, 71839),
+  (71923, 71934), (71943, 71944), (71946, 71947), (71956, 71956),
+  (71959, 71959), (71990, 71990), (71993, 71994), (72007, 72015),
+  (72026, 72095), (72104, 72105), (72152, 72153), (72165, 72191),
+  (72264, 72271), (72355, 72367), (72441, 72447), (72458, 72703),
+  (72713, 72713), (72759, 72759), (72774, 72783), (72813, 72815),
+  (72848, 72849), (72872, 72872), (72887, 72959), (72967, 72967),
+  (72970, 72970), (73015, 73017), (73019, 73019), (73022, 73022),
+  (73032, 73039), (73050, 73055), (73062, 73062), (73065, 73065),
+  (73103, 73103), (73106, 73106), (73113, 73119), (73130, 73439),
+  (73465, 73471), (73489, 73489), (73531, 73533), (73562, 73647),
+  (73649, 73663), (73714, 73726), (74650, 74751), (74863, 74863),
+  (74869, 74879), (75076, 77711), (77811, 77823), (78896, 78911),
+  (78934, 82943), (83527, 92159), (92729, 92735), (92767, 92767),
+  (92778, 92781), (92863, 92863), (92874, 92879), (92910, 92911),
+  (92918, 92927), (92998, 93007), (93018, 93018), (93026, 93026),
+  (93048, 93052), (93072, 93759), (93851, 93951), (94027, 94030),
+  (94088, 94094), (94112, 94175), (94181, 94191), (94194, 94207),
+  (100344, 100351), (101590, 101631), (101641, 110575), (110580, 110580),
+  (110588, 110588), (110591, 110591), (110883, 110897), (110899, 110927),
+  (110931, 110932), (110934, 110947), (110952, 110959), (111356, 113663),
+  (113771, 113775), (113789, 113791), (113801, 113807), (113818, 113819),
+  (113824, 118527), (118574, 118575), (118599, 118607), (118724, 118783),
+  (119030, 119039), (119079, 119080), (119155, 119162), (119275, 119295),
+  (119366, 119487), (119508, 119519), (119540, 119551), (119639, 119647),
+  (119673, 119807), (119893, 119893), (119965, 119965), (119968, 119969),
+  (119971, 119972), (119975, 119976), (119981, 119981), (119994, 119994),
+  (119996, 119996), (120004, 120004), (120070, 120070), (120075, 120076),
+  (120085, 120085), (120093, 120093), (120122, 120122), (120127, 120127),
+  (120133, 120133), (120135, 120137), (120145, 120145), (120486, 120487),
+  (120780, 120781), (121484, 121498), (121504, 121504), (121520, 122623),
+  (122655, 122660), (122667, 122879), (122887, 122887), (122905, 122906),
+  (122914, 122914), (122917, 122917), (122923, 122927), (122990, 123022),
+  (123024, 123135), (123181, 123183), (123198, 123199), (123210, 123213),
+  (123216, 123535), (123567, 123583), (123642, 123646), (123648, 124111),
+  (124154, 124895), (124903, 124903), (124908, 124908), (124911, 124911),
+  (124927, 124927), (125125, 125126), (125143, 125183), (125260, 125263),
+  (125274, 125277), (125280, 126064), (126133, 126208), (126270, 126463),
+  (126468, 126468), (126496, 126496), (126499, 126499), (126501, 126502),
+  (126504, 126504), (126515, 126515), (126520, 126520), (126522, 126522),
+  (126524, 126529), (126531, 126534), (126536, 126536), (126538, 126538),
+  (126540, 126540), (126544, 126544), (126547, 126547), (126549, 126550),
+  (126552, 126552), (126554, 126554), (126556, 126556), (126558, 126558),
+  (126560, 126560), (126563, 126563), (126565, 126566), (126571, 126571),
+  (126579, 126579), (126584, 126584), (126589, 126589), (126591, 126591),
+  (126602, 126602), (126620, 126624), (126628, 126628), (126634, 126634),
+  (126652, 126703), (126706, 126975), (127020, 127023), (127124, 127135),
+  (127151, 127152), (127168, 127168), (127184, 127184), (127222, 127231),
+  (127406, 127461), (127491, 127503), (127548, 127551), (127561, 127567),
+  (127570, 127583), (127590, 127743), (128728, 128731), (128749, 128751),
+  (128765, 128767), (128887, 128890), (128986, 128991), (129004, 129007),
+  (129009, 129023), (129036, 129039), (129096, 129103), (129114, 129119),
+  (129160, 129167), (129198, 129199), (129202, 129279), (129620, 129631),
+  (129646, 129647), (129661, 129663), (129673, 129679), (129726, 129726),
+  (129734, 129741), (129756, 129759), (129769, 129775), (129785, 129791),
+  (129939, 129939), (129995, 130031), (130042, 131071), (173792, 173823),
+  (177978, 177983), (178206, 178207), (183970, 183983), (191457, 194559),
+  (195102, 196607), (201547, 201551), (205744, 917759), (918000, 1114111),
+];
diff --git a/src/formatter.rs b/src/formatter.rs
new file mode 100644 (file)
index 0000000..3ccd23f
--- /dev/null
@@ -0,0 +1,75 @@
+use core::fmt;
+use core::fmt::Display;
+use core::mem;
+use core::result;
+
+use super::escape;
+
+/// The error type returned by [`Quote::escape`].
+///
+/// This type is used similarly to [`fmt::Error`] in the standard library.
+///
+/// [`Quote::escape`]: super::Quote::escape
+#[derive(Debug, Eq, PartialEq)]
+pub struct Error(pub(super) fmt::Error);
+
+impl Display for Error {
+    #[inline]
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+/// The type returned by [`Quote::escape`].
+///
+/// This type is used similarly to [`fmt::Result`] in the standard library.
+///
+/// [`Quote::escape`]: super::Quote::escape
+pub type Result = result::Result<(), Error>;
+
+/// The type passed between calls to [`Quote::escape`].
+///
+/// All methods of this struct are defined to ensure that strings are quoted
+/// uniformly. However, it is usually sufficient to pass this struct to the
+/// [`Quote::escape`] implementation of another type.
+///
+/// # Safety
+///
+/// Although this type is annotated with `#[repr(transparent)]`, the inner
+/// representation is not stable. Transmuting between this type and any other
+/// causes immediate undefined behavior.
+///
+/// [`Quote::escape`]: super::Quote::escape
+#[repr(transparent)]
+pub struct Formatter<'a>(pub(super) fmt::Formatter<'a>);
+
+impl<'a> Formatter<'a> {
+    pub(super) fn new<'b>(f: &'b mut fmt::Formatter<'a>) -> &'b mut Self {
+        // SAFETY: This struct has a layout that makes this operation safe.
+        unsafe { mem::transmute(f) }
+    }
+
+    /// Provides an implementation of [`Quote::escape`] for a UTF-16 string
+    /// iterator.
+    ///
+    /// The iterator does not need to contain valid UTF-16, since invalid
+    /// sequences will be escaped.
+    ///
+    /// [`Quote::escape`]: super::Quote::escape
+    #[inline]
+    pub fn escape_utf16<I>(&mut self, iter: I) -> Result
+    where
+        I: IntoIterator<Item = u16>,
+    {
+        escape::escape_utf16(iter, &mut self.0).map_err(Error)
+    }
+}
+
+#[cfg(feature = "std")]
+mod std {
+    use std::error;
+
+    use super::Error;
+
+    impl error::Error for Error {}
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..fcd38ad
--- /dev/null
@@ -0,0 +1,136 @@
+//! This crate allows quoting strings for use in output. It works similarly to
+//! [`str::escape_debug`], but the result is meant to be shown to users. Simply
+//! call [`Quote::quote`] on an argument passed to [`println!`] or a similar
+//! macro to quote it.
+//!
+//! One of the primary uses for this crate is displaying paths losslessly.
+//! Since [`Path`] has no [`Display`] implementation, it is usually output by
+//! calling [`Path::display`] or [`Path::to_string_lossy`] beforehand. However,
+//! both of those methods are lossy; they replace all invalid characters with
+//! [`REPLACEMENT_CHARACTER`]. This crate escapes those invalid characters
+//! instead, allowing them to always be displayed correctly.
+//!
+//! Unprintable characters are also escaped, to give unambiguous output. All
+//! code points are supported, but the Unicode Standard does not define which
+//! are unprintable. So, a typical subset is used that may change between minor
+//! versions. Guarantees are made in the next section.
+//!
+//! # Format
+//!
+//! The format used to represent strings is different from typical [`Debug`]
+//! output, because it is designed to show most paths correctly on any
+//! platform. In particular, backslashes (`\`) will never be escaped, since
+//! Windows uses them as directory separators. They exist in almost every path
+//! users provide on Windows.
+//!
+//! In their place, curly braces (`{` and `}`) are substituted, since they
+//! appear less frequently. Thus, normal paths should not require any escaping
+//! at all. The intention is to make the result easily readable on any system.
+//!
+//! These are some examples of the quoting format:
+//!
+//! ```
+//! use uniquote::Quote;
+//!
+//! assert_eq!(r#""foo bar""#,      "foo bar".quote().to_string());
+//! assert_eq!(r#""foo{~n}bar""#,   "foo\nbar".quote().to_string());
+//! assert_eq!(r#""foo{~u7f}bar""#, "foo\x7Fbar".quote().to_string());
+//! assert_eq!(r#""foo{"}bar""#,    "foo\"bar".quote().to_string());
+//! ```
+//!
+//! The only ASCII characters escaped are `"`, `{`, `}`, and [control
+//! characters]. Other characters are not guaranteed to be quoted in a specific
+//! way but will generally only be escaped if unprintable.
+//!
+//! # Features
+//!
+//! These features are optional and can be enabled or disabled in a
+//! "Cargo.toml" file.
+//!
+//! ### Default Features
+//!
+//! - **alloc** -
+//!   Provides implementations of [`Quote`] for types that require allocation.
+//!   This feature is enabled automatically when the **std** feature is
+//!   enabled.
+//!
+//! - **std** -
+//!   Provides implementations of [`Quote`] for types that require the standard
+//!   library. When this feature is disabled, this crate can be used in
+//!   `#![no_std]` environments.
+//!
+//! # Examples
+//!
+//! **Printing Command Line Arguments:**
+//!
+//! ```
+//! use std::env;
+//!
+//! use uniquote::Quote;
+//!
+//! for (i, arg) in env::args_os().enumerate() {
+//! #   #[cfg(feature = "std")]
+//!     println!("arg #{} is {}", i, arg.quote());
+//! }
+//! ```
+//!
+//! **Creating a Descriptive Error Message:**
+//!
+//! ```
+//! use std::error::Error;
+//! use std::fmt;
+//! use std::fmt::Display;
+//! use std::fmt::Formatter;
+//! use std::path::PathBuf;
+//!
+//! use uniquote::Quote;
+//!
+//! #[derive(Debug)]
+//! struct FileNotFoundError(PathBuf);
+//!
+//! # #[cfg(feature = "std")]
+//! # {
+//! impl Display for FileNotFoundError {
+//!     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+//!         write!(f, "file not found at {}", self.0.quote())
+//!     }
+//! }
+//!
+//! impl Error for FileNotFoundError {}
+//! # }
+//! ```
+//!
+//! [control characters]: char::is_ascii_control
+//! [`Debug`]: ::std::fmt::Debug
+//! [`Display`]: ::std::fmt::Display
+//! [`Path`]: ::std::path::Path
+//! [`Path::display`]: ::std::path::Path::display
+//! [`Path::to_string_lossy`]: ::std::path::Path::to_string_lossy
+//! [`REPLACEMENT_CHARACTER`]: ::std::char::REPLACEMENT_CHARACTER
+
+// Nightly is also currently required for the SGX platform.
+#![cfg_attr(
+    all(feature = "std", target_vendor = "fortanix", target_env = "sgx"),
+    feature(sgx_platform)
+)]
+#![cfg_attr(not(feature = "std"), no_std)]
+#![warn(unused_results)]
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+mod escape;
+
+mod formatter;
+pub use formatter::Error;
+pub use formatter::Formatter;
+pub use formatter::Result;
+
+mod quote;
+pub use quote::Quote;
+
+const QUOTE: char = '"';
+
+const START_ESCAPE: char = '{';
+
+const END_ESCAPE: char = '}';
diff --git a/src/quote.rs b/src/quote.rs
new file mode 100644 (file)
index 0000000..617a34f
--- /dev/null
@@ -0,0 +1,208 @@
+use core::ffi::CStr;
+use core::fmt;
+use core::fmt::Write as _;
+
+use super::Error;
+use super::Formatter;
+use super::Result;
+use super::QUOTE;
+
+#[derive(Debug)]
+pub struct Display<T>(T);
+
+impl<T> fmt::Display for Display<&T>
+where
+    T: Quote + ?Sized,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_char(QUOTE)?;
+
+        self.0.escape(Formatter::new(f)).map_err(|x| x.0)?;
+
+        f.write_char(QUOTE)
+    }
+}
+
+/// The trait used to quote strings.
+pub trait Quote {
+    /// Escapes a string using the format described in the [the module-level
+    /// documentation][format], without the surrounding quotes.
+    ///
+    /// This method is only used to provide new implementations of this trait.
+    ///
+    /// # Errors
+    ///
+    /// Similarly to [`Display::fmt`], this method should fail if and only if
+    /// the formatter returns an error. Since quoting is an infallible
+    /// operation, these failures will only result from inability to write to
+    /// the underlying stream.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use uniquote::Quote;
+    ///
+    /// struct Strings<'a>(&'a str, &'a str);
+    ///
+    /// impl Quote for Strings<'_> {
+    ///     fn escape(&self, f: &mut uniquote::Formatter<'_>) -> uniquote::Result {
+    ///         self.0.escape(f)?;
+    ///         ','.escape(f)?;
+    ///         self.1.escape(f)
+    ///     }
+    /// }
+    ///
+    /// assert_eq!(r#""foo,bar""#, Strings("foo", "bar").quote().to_string());
+    /// ```
+    ///
+    /// [`Display::fmt`]: fmt::Display::fmt
+    /// [format]: super#format
+    fn escape(&self, f: &mut Formatter<'_>) -> Result;
+
+    /// Quotes a string using the format described in the [the module-level
+    /// documentation][format].
+    ///
+    /// The returned struct will implement [`Display`]. It can be output using
+    /// a formatting macro or converted to a string by calling
+    /// [`ToString::to_string`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use std::env;
+    /// # use std::io;
+    ///
+    /// use uniquote::Quote;
+    ///
+    /// # #[cfg(feature = "std")]
+    /// println!("{}", env::current_exe()?.quote());
+    /// #
+    /// # Ok::<_, io::Error>(())
+    /// ```
+    ///
+    /// [`Display`]: fmt::Display
+    /// [format]: super#format
+    #[inline]
+    #[must_use]
+    fn quote(&self) -> Display<&Self> {
+        Display(self)
+    }
+}
+
+macro_rules! r#impl {
+    ( $($type:ty),+ ) => {
+        $(
+            impl Quote for $type {
+                #[inline]
+                fn escape(&self, f: &mut Formatter<'_>) -> $crate::Result {
+                    use super::escape::Escape;
+
+                    Escape::escape(self, &mut f.0).map_err(Error)
+                }
+            }
+        )+
+    };
+}
+r#impl!(char, str, [u8]);
+
+impl<const N: usize> Quote for [u8; N] {
+    #[inline]
+    fn escape(&self, f: &mut Formatter<'_>) -> Result {
+        self[..].escape(f)
+    }
+}
+
+#[cfg(feature = "alloc")]
+macro_rules! impl_with_deref {
+    ( $($type:ty),+ ) => {
+        $(
+            impl $crate::Quote for $type {
+                #[inline]
+                fn escape(
+                    &self,
+                    f: &mut $crate::Formatter<'_>
+                ) -> $crate::Result {
+                    (**self).escape(f)
+                }
+            }
+        )+
+    };
+}
+
+impl Quote for CStr {
+    #[inline]
+    fn escape(&self, f: &mut Formatter<'_>) -> Result {
+        self.to_bytes().escape(f)
+    }
+}
+
+#[cfg(feature = "alloc")]
+mod alloc {
+    use alloc::ffi::CString;
+    use alloc::string::String;
+    use alloc::vec::Vec;
+
+    impl_with_deref!(CString, String, Vec<u8>);
+}
+
+#[cfg(feature = "std")]
+mod std {
+    #[cfg(any(
+        all(target_vendor = "fortanix", target_env = "sgx"),
+        target_os = "hermit",
+        target_os = "solid_asp3",
+        target_os = "wasi",
+        unix,
+        windows,
+    ))]
+    mod os_str {
+        use std::ffi::OsStr;
+        use std::ffi::OsString;
+        use std::path::Path;
+        use std::path::PathBuf;
+
+        use crate::Formatter;
+        use crate::Result;
+
+        use super::super::Quote;
+
+        impl Quote for OsStr {
+            #[inline]
+            fn escape(&self, f: &mut Formatter<'_>) -> Result {
+                #[cfg(windows)]
+                {
+                    use std::os::windows::ffi::OsStrExt;
+
+                    f.escape_utf16(self.encode_wide())
+                }
+                #[cfg(not(windows))]
+                {
+                    #[cfg(all(
+                        target_vendor = "fortanix",
+                        target_env = "sgx",
+                    ))]
+                    use std::os::fortanix_sgx as os;
+                    #[cfg(target_os = "solid_asp3")]
+                    use std::os::solid as os;
+                    #[cfg(any(target_os = "hermit", unix))]
+                    use std::os::unix as os;
+                    #[cfg(target_os = "wasi")]
+                    use std::os::wasi as os;
+
+                    use os::ffi::OsStrExt;
+
+                    self.as_bytes().escape(f)
+                }
+            }
+        }
+
+        impl Quote for Path {
+            #[inline]
+            fn escape(&self, f: &mut Formatter<'_>) -> Result {
+                self.as_os_str().escape(f)
+            }
+        }
+
+        impl_with_deref!(OsString, PathBuf);
+    }
+}