Skip to content

Commit 86edca9

Browse files
committed
Support bech32 encoding without a checksum
BOLT 12 Offers 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.
1 parent 0ef5bb9 commit 86edca9

File tree

1 file changed

+104
-23
lines changed

1 file changed

+104
-23
lines changed

src/lib.rs

+104-23
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,12 @@ impl<'a> Bech32Writer<'a> {
182182

183183
/// Write out the checksum at the end. If this method isn't called this will happen on drop.
184184
pub fn finalize(mut self) -> fmt::Result {
185-
self.inner_finalize()?;
185+
self.write_checksum()?;
186186
mem::forget(self);
187187
Ok(())
188188
}
189189

190-
fn inner_finalize(&mut self) -> fmt::Result {
190+
fn write_checksum(&mut self) -> fmt::Result {
191191
// Pad with 6 zeros
192192
for _ in 0..CHECKSUM_LENGTH {
193193
self.polymod_step(u5(0))
@@ -203,6 +203,7 @@ impl<'a> Bech32Writer<'a> {
203203
Ok(())
204204
}
205205
}
206+
206207
impl<'a> WriteBase32 for Bech32Writer<'a> {
207208
type Err = fmt::Error;
208209

@@ -215,7 +216,7 @@ impl<'a> WriteBase32 for Bech32Writer<'a> {
215216

216217
impl<'a> Drop for Bech32Writer<'a> {
217218
fn drop(&mut self) {
218-
self.inner_finalize()
219+
self.write_checksum()
219220
.expect("Unhandled error writing the checksum on drop.")
220221
}
221222
}
@@ -421,6 +422,32 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(
421422
})?)
422423
}
423424

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())?;
447+
}
448+
Ok(())
449+
}
450+
424451
/// Used for encode/decode operations for the two variants of Bech32
425452
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
426453
pub enum Variant {
@@ -463,15 +490,48 @@ pub fn encode<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<St
463490
Ok(buf)
464491
}
465492

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)?;
502+
Ok(buf)
503+
}
504+
466505
/// Decode a bech32 string into the raw HRP and the data bytes.
467506
///
468-
/// Returns the HRP in lowercase..
507+
/// Returns the HRP in lowercase, the data with the checksum removed, and the encoding.
469508
pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
470-
// Ensure overall length is within bounds
471-
if s.len() < CHECKSUM_LENGTH + 2 {
509+
let (hrp_lower, mut data) = split_and_decode(s)?;
510+
if data.len() < CHECKSUM_LENGTH {
472511
return Err(Error::InvalidLength);
473512
}
474513

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> {
475535
// Split at separator and check for two pieces
476536
let (raw_hrp, raw_data) = match s.rfind(SEP) {
477537
None => return Err(Error::MissingSeparator),
@@ -480,9 +540,6 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
480540
(hrp, &data[1..])
481541
}
482542
};
483-
if raw_data.len() < CHECKSUM_LENGTH {
484-
return Err(Error::InvalidLength);
485-
}
486543

487544
let mut case = check_hrp(raw_hrp)?;
488545
let hrp_lower = match case {
@@ -492,7 +549,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
492549
};
493550

494551
// Check data payload
495-
let mut data = raw_data
552+
let data = raw_data
496553
.chars()
497554
.map(|c| {
498555
// Only check if c is in the ASCII range, all invalid ASCII
@@ -527,17 +584,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
527584
})
528585
.collect::<Result<Vec<u5>, Error>>()?;
529586

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

543590
fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
@@ -801,6 +848,8 @@ mod tests {
801848
Error::InvalidLength),
802849
("1p2gdwpf",
803850
Error::InvalidLength),
851+
("bc1p2",
852+
Error::InvalidLength),
804853
);
805854
for p in pairs {
806855
let (s, expected_error) = p;
@@ -930,7 +979,7 @@ mod tests {
930979
}
931980

932981
#[test]
933-
fn writer() {
982+
fn write_with_checksum() {
934983
let hrp = "lnbc";
935984
let data = "Hello World!".as_bytes().to_base32();
936985

@@ -947,7 +996,26 @@ mod tests {
947996
}
948997

949998
#[test]
950-
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() {
9511019
let hrp = "lntb";
9521020
let data = "Hello World!".as_bytes().to_base32();
9531021

@@ -962,6 +1030,19 @@ mod tests {
9621030
assert_eq!(encoded_str, written_str);
9631031
}
9641032

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+
9651046
#[test]
9661047
fn test_hrp_case() {
9671048
// Tests for issue with HRP case checking being ignored for encoding

0 commit comments

Comments
 (0)