Skip to content

Commit 6df4a67

Browse files
committed
Merge #66: Support bech32 encoding without a checksum
86edca9 Support bech32 encoding without a checksum (Jeffrey Czyz) 0ef5bb9 Add an Error variant for fmt::Error (Jeffrey Czyz) 1735cb3 Use a constant for checksum length (Jeffrey Czyz) Pull request description: [BOLT 12 Offers](lightning/bolts#798) uses bech32 encoding without a checksum since QR codes already have a checksum. Add functions `encode_without_checksum` and `decode_without_checksum` to support this use case. Also, remove overall length check in decode since it is unnecessary. ACKs for top commit: apoelstra: ACK 86edca9 Tree-SHA512: f45cbc03caa615a2377f183a8e90931ac624c4321b0f0a113b743f3bfc435d27881465c7614face729bcfba8fefbf300dde49fe9024a298474e2798c742ff806
2 parents fa306e6 + 86edca9 commit 6df4a67

File tree

1 file changed

+125
-35
lines changed

1 file changed

+125
-35
lines changed

src/lib.rs

+125-35
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub trait WriteBase32 {
128128
fn write_u5(&mut self, data: u5) -> Result<(), Self::Err>;
129129
}
130130

131+
const CHECKSUM_LENGTH: usize = 6;
132+
131133
/// Allocationless Bech32 writer that accumulates the checksum data internally and writes them out
132134
/// in the end.
133135
pub struct Bech32Writer<'a> {
@@ -180,27 +182,28 @@ impl<'a> Bech32Writer<'a> {
180182

181183
/// Write out the checksum at the end. If this method isn't called this will happen on drop.
182184
pub fn finalize(mut self) -> fmt::Result {
183-
self.inner_finalize()?;
185+
self.write_checksum()?;
184186
mem::forget(self);
185187
Ok(())
186188
}
187189

188-
fn inner_finalize(&mut self) -> fmt::Result {
190+
fn write_checksum(&mut self) -> fmt::Result {
189191
// Pad with 6 zeros
190-
for _ in 0..6 {
192+
for _ in 0..CHECKSUM_LENGTH {
191193
self.polymod_step(u5(0))
192194
}
193195

194196
let plm: u32 = self.chk ^ self.variant.constant();
195197

196-
for p in 0..6 {
198+
for p in 0..CHECKSUM_LENGTH {
197199
self.formatter
198200
.write_char(u5(((plm >> (5 * (5 - p))) & 0x1f) as u8).to_char())?;
199201
}
200202

201203
Ok(())
202204
}
203205
}
206+
204207
impl<'a> WriteBase32 for Bech32Writer<'a> {
205208
type Err = fmt::Error;
206209

@@ -213,7 +216,7 @@ impl<'a> WriteBase32 for Bech32Writer<'a> {
213216

214217
impl<'a> Drop for Bech32Writer<'a> {
215218
fn drop(&mut self) {
216-
self.inner_finalize()
219+
self.write_checksum()
217220
.expect("Unhandled error writing the checksum on drop.")
218221
}
219222
}
@@ -398,28 +401,51 @@ fn check_hrp(hrp: &str) -> Result<Case, Error> {
398401
///
399402
/// # Errors
400403
/// * If [check_hrp] returns an error for the given HRP.
404+
/// * If `fmt` fails on write
401405
/// # Deviations from standard
402406
/// * No length limits are enforced for the data part
403407
pub fn encode_to_fmt<T: AsRef<[u5]>>(
404408
fmt: &mut fmt::Write,
405409
hrp: &str,
406410
data: T,
407411
variant: Variant,
408-
) -> Result<fmt::Result, Error> {
412+
) -> Result<(), Error> {
409413
let hrp_lower = match check_hrp(hrp)? {
410414
Case::Upper => Cow::Owned(hrp.to_lowercase()),
411415
Case::Lower | Case::None => Cow::Borrowed(hrp),
412416
};
413417

414-
match Bech32Writer::new(&hrp_lower, variant, fmt) {
415-
Ok(mut writer) => {
416-
Ok(writer.write(data.as_ref()).and_then(|_| {
417-
// Finalize manually to avoid panic on drop if write fails
418-
writer.finalize()
419-
}))
420-
}
421-
Err(e) => Ok(Err(e)),
418+
let mut writer = Bech32Writer::new(&hrp_lower, variant, fmt)?;
419+
Ok(writer.write(data.as_ref()).and_then(|_| {
420+
// Finalize manually to avoid panic on drop if write fails
421+
writer.finalize()
422+
})?)
423+
}
424+
425+
/// Encode a bech32 payload without a checksum to an [fmt::Write].
426+
/// This method is intended for implementing traits from [std::fmt].
427+
///
428+
/// # Errors
429+
/// * If [check_hrp] returns an error for the given HRP.
430+
/// * If `fmt` fails on write
431+
/// # Deviations from standard
432+
/// * No length limits are enforced for the data part
433+
pub fn encode_without_checksum_to_fmt<T: AsRef<[u5]>>(
434+
fmt: &mut fmt::Write,
435+
hrp: &str,
436+
data: T,
437+
) -> Result<(), Error> {
438+
let hrp = match check_hrp(hrp)? {
439+
Case::Upper => Cow::Owned(hrp.to_lowercase()),
440+
Case::Lower | Case::None => Cow::Borrowed(hrp),
441+
};
442+
443+
fmt.write_str(&hrp)?;
444+
fmt.write_char(SEP)?;
445+
for b in data.as_ref() {
446+
fmt.write_char(b.to_char())?;
422447
}
448+
Ok(())
423449
}
424450

425451
/// Used for encode/decode operations for the two variants of Bech32
@@ -460,19 +486,52 @@ impl Variant {
460486
/// * No length limits are enforced for the data part
461487
pub fn encode<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<String, Error> {
462488
let mut buf = String::new();
463-
encode_to_fmt(&mut buf, hrp, data, variant)?.unwrap();
489+
encode_to_fmt(&mut buf, hrp, data, variant)?;
490+
Ok(buf)
491+
}
492+
493+
/// Encode a bech32 payload to string without the checksum.
494+
///
495+
/// # Errors
496+
/// * If [check_hrp] returns an error for the given HRP.
497+
/// # Deviations from standard
498+
/// * No length limits are enforced for the data part
499+
pub fn encode_without_checksum<T: AsRef<[u5]>>(hrp: &str, data: T) -> Result<String, Error> {
500+
let mut buf = String::new();
501+
encode_without_checksum_to_fmt(&mut buf, hrp, data)?;
464502
Ok(buf)
465503
}
466504

467505
/// Decode a bech32 string into the raw HRP and the data bytes.
468506
///
469-
/// Returns the HRP in lowercase..
507+
/// Returns the HRP in lowercase, the data with the checksum removed, and the encoding.
470508
pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
471-
// Ensure overall length is within bounds
472-
if s.len() < 8 {
509+
let (hrp_lower, mut data) = split_and_decode(s)?;
510+
if data.len() < CHECKSUM_LENGTH {
473511
return Err(Error::InvalidLength);
474512
}
475513

514+
// Ensure checksum
515+
match verify_checksum(hrp_lower.as_bytes(), &data) {
516+
Some(variant) => {
517+
// Remove checksum from data payload
518+
data.truncate(data.len() - CHECKSUM_LENGTH);
519+
520+
Ok((hrp_lower, data, variant))
521+
}
522+
None => Err(Error::InvalidChecksum),
523+
}
524+
}
525+
526+
/// Decode a bech32 string into the raw HRP and the data bytes, assuming no checksum.
527+
///
528+
/// Returns the HRP in lowercase and the data.
529+
pub fn decode_without_checksum(s: &str) -> Result<(String, Vec<u5>), Error> {
530+
split_and_decode(s)
531+
}
532+
533+
/// Decode a bech32 string into the raw HRP and the `u5` data.
534+
fn split_and_decode(s: &str) -> Result<(String, Vec<u5>), Error> {
476535
// Split at separator and check for two pieces
477536
let (raw_hrp, raw_data) = match s.rfind(SEP) {
478537
None => return Err(Error::MissingSeparator),
@@ -481,9 +540,6 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
481540
(hrp, &data[1..])
482541
}
483542
};
484-
if raw_data.len() < 6 {
485-
return Err(Error::InvalidLength);
486-
}
487543

488544
let mut case = check_hrp(raw_hrp)?;
489545
let hrp_lower = match case {
@@ -493,7 +549,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
493549
};
494550

495551
// Check data payload
496-
let mut data = raw_data
552+
let data = raw_data
497553
.chars()
498554
.map(|c| {
499555
// Only check if c is in the ASCII range, all invalid ASCII
@@ -528,17 +584,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
528584
})
529585
.collect::<Result<Vec<u5>, Error>>()?;
530586

531-
// Ensure checksum
532-
match verify_checksum(hrp_lower.as_bytes(), &data) {
533-
Some(variant) => {
534-
// Remove checksum from data payload
535-
let dbl: usize = data.len();
536-
data.truncate(dbl - 6);
537-
538-
Ok((hrp_lower, data, variant))
539-
}
540-
None => Err(Error::InvalidChecksum),
541-
}
587+
Ok((hrp_lower, data))
542588
}
543589

544590
fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
@@ -622,6 +668,14 @@ pub enum Error {
622668
InvalidPadding,
623669
/// The whole string must be of one case
624670
MixedCase,
671+
/// Writing UTF-8 data failed
672+
WriteFailure(fmt::Error),
673+
}
674+
675+
impl From<fmt::Error> for Error {
676+
fn from(error: fmt::Error) -> Self {
677+
Self::WriteFailure(error)
678+
}
625679
}
626680

627681
impl fmt::Display for Error {
@@ -634,6 +688,7 @@ impl fmt::Display for Error {
634688
Error::InvalidData(n) => write!(f, "invalid data point ({})", n),
635689
Error::InvalidPadding => write!(f, "invalid padding"),
636690
Error::MixedCase => write!(f, "mixed-case strings not allowed"),
691+
Error::WriteFailure(_) => write!(f, "failed writing utf-8 data"),
637692
}
638693
}
639694
}
@@ -649,6 +704,7 @@ impl std::error::Error for Error {
649704
Error::InvalidData(_) => "invalid data point",
650705
Error::InvalidPadding => "invalid padding",
651706
Error::MixedCase => "mixed-case strings not allowed",
707+
Error::WriteFailure(_) => "failed writing utf-8 data",
652708
}
653709
}
654710
}
@@ -792,6 +848,8 @@ mod tests {
792848
Error::InvalidLength),
793849
("1p2gdwpf",
794850
Error::InvalidLength),
851+
("bc1p2",
852+
Error::InvalidLength),
795853
);
796854
for p in pairs {
797855
let (s, expected_error) = p;
@@ -921,7 +979,7 @@ mod tests {
921979
}
922980

923981
#[test]
924-
fn writer() {
982+
fn write_with_checksum() {
925983
let hrp = "lnbc";
926984
let data = "Hello World!".as_bytes().to_base32();
927985

@@ -938,7 +996,26 @@ mod tests {
938996
}
939997

940998
#[test]
941-
fn write_on_drop() {
999+
fn write_without_checksum() {
1000+
let hrp = "lnbc";
1001+
let data = "Hello World!".as_bytes().to_base32();
1002+
1003+
let mut written_str = String::new();
1004+
{
1005+
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, &mut written_str).unwrap();
1006+
writer.write(&data).unwrap();
1007+
}
1008+
1009+
let encoded_str = encode_without_checksum(hrp, data).unwrap();
1010+
1011+
assert_eq!(
1012+
encoded_str,
1013+
written_str[..written_str.len() - CHECKSUM_LENGTH]
1014+
);
1015+
}
1016+
1017+
#[test]
1018+
fn write_with_checksum_on_drop() {
9421019
let hrp = "lntb";
9431020
let data = "Hello World!".as_bytes().to_base32();
9441021

@@ -953,6 +1030,19 @@ mod tests {
9531030
assert_eq!(encoded_str, written_str);
9541031
}
9551032

1033+
#[test]
1034+
fn roundtrip_without_checksum() {
1035+
let hrp = "lnbc";
1036+
let data = "Hello World!".as_bytes().to_base32();
1037+
1038+
let encoded = encode_without_checksum(hrp, data.clone()).expect("failed to encode");
1039+
let (decoded_hrp, decoded_data) =
1040+
decode_without_checksum(&encoded).expect("failed to decode");
1041+
1042+
assert_eq!(decoded_hrp, hrp);
1043+
assert_eq!(decoded_data, data);
1044+
}
1045+
9561046
#[test]
9571047
fn test_hrp_case() {
9581048
// Tests for issue with HRP case checking being ignored for encoding

0 commit comments

Comments
 (0)