Repository: rust-bitcoin/rust-bitcoin (language: rust)

Issue:
units: Remove `internals::InputString` from the public API 
`internals` is not stable and is not going to be. Nothing from `internals` should appear in the public API of any of the leaf crates.

**Fail**

`core::convert::Into<bitcoin_internals::error::input_string::InputString>>(s: S) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>`

Also the `parse::int` function

```rust
pub fn int<T: Integer, S: AsRef<str> + Into<InputString>>(s: S) -> Result<T, ParseIntError> {
```

As a rule so this doesn't happen again I don't think we should put types into `internals` - period.

This issue will likely be solved as part of #3732


Discussion / hints:
That attempt is pretty far from even being close to correct.
>If you no longer want Latta AI to attempt solving issues on your repository, you can block this account.

Will keep an eye out for more of these. Glad they're open about what they're doing at least. Probably they are gonna get site-banned if they're using "thousands of users should all block me" as a feedback mechanism lol.
> As a rule so this doesn't happen again I don't think we should put types into `internals` - period.

This seems overly strong and also not strong enough to prevent problems. We would also need to ban traits in `internals` and arguably macros. We could just have a rule that we're not allowed to import types or traits and have to refer to them by their `interals::` prefix, which would make them visible in the API files.


That gave me an idea. We don't need to worry about how we import because the api files use the full path. We can just add a CI job that greps the api files and fails if it finds `bitcoin_internals` - WIN!
Related: #3322

Fix patch:
diff --git a/api/units/all-features.txt b/api/units/all-features.txt
index 0559a49bc..5698c7c89 100644
--- a/api/units/all-features.txt
+++ b/api/units/all-features.txt
@@ -1183,7 +1183,9 @@ pub fn bitcoin_units::parse::hex_u32(s: &str) -> core::result::Result<u32, bitco
 pub fn bitcoin_units::parse::hex_u32_prefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::PrefixedHexError>
 pub fn bitcoin_units::parse::hex_u32_unchecked(s: &str) -> core::result::Result<u32, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::parse::hex_u32_unprefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::UnprefixedHexError>
-pub fn bitcoin_units::parse::int<T: bitcoin_units::parse::Integer, S: core::convert::AsRef<str> + core::convert::Into<bitcoin_internals::error::input_string::InputString>>(s: S) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_box<T: bitcoin_units::parse::Integer>(s: alloc::boxed::Box<str>) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_str<T: bitcoin_units::parse::Integer>(s: &str) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_string<T: bitcoin_units::parse::Integer>(s: alloc::string::String) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::weight::Weight::add(self, rhs: &bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add_assign(&mut self, rhs: &bitcoin_units::weight::Weight)
diff --git a/api/units/alloc-only.txt b/api/units/alloc-only.txt
index a44702c7a..6274ad864 100644
--- a/api/units/alloc-only.txt
+++ b/api/units/alloc-only.txt
@@ -1030,7 +1030,9 @@ pub fn bitcoin_units::parse::hex_u32(s: &str) -> core::result::Result<u32, bitco
 pub fn bitcoin_units::parse::hex_u32_prefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::PrefixedHexError>
 pub fn bitcoin_units::parse::hex_u32_unchecked(s: &str) -> core::result::Result<u32, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::parse::hex_u32_unprefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::UnprefixedHexError>
-pub fn bitcoin_units::parse::int<T: bitcoin_units::parse::Integer, S: core::convert::AsRef<str> + core::convert::Into<bitcoin_internals::error::input_string::InputString>>(s: S) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_box<T: bitcoin_units::parse::Integer>(s: alloc::boxed::Box<str>) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_str<T: bitcoin_units::parse::Integer>(s: &str) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_string<T: bitcoin_units::parse::Integer>(s: alloc::string::String) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::weight::Weight::add(self, rhs: &bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add_assign(&mut self, rhs: &bitcoin_units::weight::Weight)
diff --git a/api/units/no-features.txt b/api/units/no-features.txt
index 39e957f3e..5c448c09e 100644
--- a/api/units/no-features.txt
+++ b/api/units/no-features.txt
@@ -986,7 +986,7 @@ pub fn bitcoin_units::parse::hex_u32(s: &str) -> core::result::Result<u32, bitco
 pub fn bitcoin_units::parse::hex_u32_prefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::PrefixedHexError>
 pub fn bitcoin_units::parse::hex_u32_unchecked(s: &str) -> core::result::Result<u32, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::parse::hex_u32_unprefixed(s: &str) -> core::result::Result<u32, bitcoin_units::parse::UnprefixedHexError>
-pub fn bitcoin_units::parse::int<T: bitcoin_units::parse::Integer, S: core::convert::AsRef<str> + core::convert::Into<bitcoin_internals::error::input_string::InputString>>(s: S) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
+pub fn bitcoin_units::parse::int_from_str<T: bitcoin_units::parse::Integer>(s: &str) -> core::result::Result<T, bitcoin_units::parse::ParseIntError>
 pub fn bitcoin_units::weight::Weight::add(self, rhs: &bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add(self, rhs: bitcoin_units::weight::Weight) -> Self::Output
 pub fn bitcoin_units::weight::Weight::add_assign(&mut self, rhs: &bitcoin_units::weight::Weight)
diff --git a/bitcoin/src/blockdata/script/witness_version.rs b/bitcoin/src/blockdata/script/witness_version.rs
index fa4f06d48..3c8f408db 100644
--- a/bitcoin/src/blockdata/script/witness_version.rs
+++ b/bitcoin/src/blockdata/script/witness_version.rs
@@ -82,7 +82,7 @@ impl FromStr for WitnessVersion {
     type Err = FromStrError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let version: u8 = parse::int(s)?;
+        let version: u8 = parse::int_from_str(s)?;
         Ok(WitnessVersion::try_from(version)?)
     }
 }
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 5dea49ba9..8a497ffd2 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -1214,7 +1214,7 @@ mod tests {
         assert_eq!(
             "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:lol"
                 .parse::<OutPoint>(),
-            Err(ParseOutPointError::Vout(parse::int::<u32, _>("lol").unwrap_err()))
+            Err(ParseOutPointError::Vout(parse::int_from_str::<u32>("lol").unwrap_err()))
         );
 
         assert_eq!(
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 21c11b472..da1a714ac 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -239,8 +239,8 @@ pub mod parse {
     #[doc(inline)]
     pub use units::parse::{
         hex_check_unprefixed, hex_remove_prefix, hex_u128, hex_u128_unchecked, hex_u128_unprefixed,
-        hex_u32, hex_u32_unchecked, hex_u32_unprefixed, int, ParseIntError, PrefixedHexError,
-        UnprefixedHexError,
+        hex_u32, hex_u32_unchecked, hex_u32_unprefixed, int_from_box, int_from_str,
+        int_from_string, ParseIntError, PrefixedHexError, UnprefixedHexError,
     };
 }
 
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index 4424103f4..bb4005798 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -424,7 +424,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
             return Err(ParseOutPointError::VoutNotCanonical);
         }
     }
-    parse::int(s).map_err(ParseOutPointError::Vout)
+    parse::int_from_str(s).map_err(ParseOutPointError::Vout)
 }
 
 /// An error in parsing an [`OutPoint`].
diff --git a/units/src/parse.rs b/units/src/parse.rs
index 08f0cf464..130a29462 100644
--- a/units/src/parse.rs
+++ b/units/src/parse.rs
@@ -76,9 +76,28 @@ mod sealed {
 
 /// Parses the input string as an integer returning an error carrying rich context.
 ///
-/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
-/// owned since it avoids allocation in error case.
-pub fn int<T: Integer, S: AsRef<str> + Into<InputString>>(s: S) -> Result<T, ParseIntError> {
+/// On error this function allocates to copy the input string into the error return. If the caller
+/// has a `String` or `Box<str>` which is not used later it's better to call
+/// [`parse::int_from_string`] or [`parse::int_from_box`] respectively.
+///
+/// [`parse::int_from_string`]: crate::parse::int_from_string
+/// [`parse::int_from_box`]: crate::parse::int_from_box
+pub fn int_from_str<T: Integer>(s: &str) -> Result<T, ParseIntError> { int(s) }
+
+/// Parses the input string as an integer returning an error carrying rich context.
+///
+/// On error the input string is moved into the error return without allocating.
+#[cfg(feature = "alloc")]
+pub fn int_from_string<T: Integer>(s: alloc::string::String) -> Result<T, ParseIntError> { int(s) }
+
+/// Parses the input string as an integer returning an error carrying rich context.
+///
+/// On error the input string is converted into the error return without allocating.
+#[cfg(feature = "alloc")]
+pub fn int_from_box<T: Integer>(s: alloc::boxed::Box<str>) -> Result<T, ParseIntError> { int(s) }
+
+// This must be private because we do not want `InputString` to appear in the public API.
+fn int<T: Integer, S: AsRef<str> + Into<InputString>>(s: S) -> Result<T, ParseIntError> {
     s.as_ref().parse().map_err(|error| {
         ParseIntError {
             input: s.into(),
@@ -120,37 +139,44 @@ pub fn int<T: Integer, S: AsRef<str> + Into<InputString>>(s: S) -> Result<T, Par
 #[doc(hidden)] // This is an 'internal' macro that should not be used outside of the `rust-bitcoin` crate.
 macro_rules! impl_parse_str_from_int_infallible {
     ($to:ident, $inner:ident, $fn:ident) => {
-        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn);
-        #[cfg(feature = "alloc")]
-        $crate::impl_tryfrom_str_from_int_infallible!(alloc::string::String, $to, $inner, $fn; alloc::boxed::Box<str>, $to, $inner, $fn);
-
         impl $crate::_export::_core::str::FromStr for $to {
             type Err = $crate::parse::ParseIntError;
 
             fn from_str(s: &str) -> $crate::_export::_core::result::Result<Self, Self::Err> {
-                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
+                $crate::_export::_core::convert::TryFrom::try_from(s)
             }
         }
 
-    }
-}
+        impl $crate::_export::_core::convert::TryFrom<&str> for $to {
+            type Error = $crate::parse::ParseIntError;
 
-/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
-/// conversion function `fn`.
-#[macro_export]
-#[doc(hidden)] // Helper macro called by `impl_parse_str_from_int_infallible`.
-macro_rules! impl_tryfrom_str_from_int_infallible {
-    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
-        $(
-        impl $crate::_export::_core::convert::TryFrom<$from> for $to {
+            fn try_from(s: &str) -> $crate::_export::_core::result::Result<Self, Self::Error> {
+                $crate::parse::int_from_str::<$inner>(s).map($to::$fn)
+            }
+        }
+
+        #[cfg(feature = "alloc")]
+        impl $crate::_export::_core::convert::TryFrom<alloc::string::String> for $to {
             type Error = $crate::parse::ParseIntError;
 
-            fn try_from(s: $from) -> $crate::_export::_core::result::Result<Self, Self::Error> {
-                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
+            fn try_from(
+                s: alloc::string::String,
+            ) -> $crate::_export::_core::result::Result<Self, Self::Error> {
+                $crate::parse::int_from_string::<$inner>(s).map($to::$fn)
             }
         }
-        )*
-    }
+
+        #[cfg(feature = "alloc")]
+        impl $crate::_export::_core::convert::TryFrom<alloc::boxed::Box<str>> for $to {
+            type Error = $crate::parse::ParseIntError;
+
+            fn try_from(
+                s: alloc::boxed::Box<str>,
+            ) -> $crate::_export::_core::result::Result<Self, Self::Error> {
+                $crate::parse::int_from_box::<$inner>(s).map($to::$fn)
+            }
+        }
+    };
 }
 
 /// Implements standard parsing traits for `$type` by calling through to `$inner_fn`.
@@ -497,9 +523,9 @@ mod tests {
 
     #[test]
     fn parse_int() {
-        assert!(int::<u8, _>("1").is_ok());
-        let _ = int::<i8, _>("not a number").map_err(|e| assert!(e.is_signed));
-        let _ = int::<u8, _>("not a number").map_err(|e| assert!(!e.is_signed));
+        assert!(int_from_str::<u8>("1").is_ok());
+        let _ = int_from_str::<i8>("not a number").map_err(|e| assert!(e.is_signed));
+        let _ = int_from_str::<u8>("not a number").map_err(|e| assert!(!e.is_signed));
     }
 
     #[test]
@@ -520,7 +546,7 @@ mod tests {
             fn from(_: i8) -> Self { TestTypeLargerThanU128(0, 0) }
         }
 
-        let result = panic::catch_unwind(|| int::<TestTypeLargerThanU128, _>("not a number"));
+        let result = panic::catch_unwind(|| int_from_str::<TestTypeLargerThanU128>("not a number"));
         assert!(result.is_err());
     }
 


Test patch: