Skip to content

Commit 82367c8

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 9a57c97 commit 82367c8

File tree

1 file changed

+91
-24
lines changed

1 file changed

+91
-24
lines changed

src/lib.rs

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub struct Bech32Writer<'a> {
134134
formatter: &'a mut fmt::Write,
135135
chk: u32,
136136
variant: Variant,
137+
checksum: Checksum,
137138
}
138139

139140
impl<'a> Bech32Writer<'a> {
@@ -144,12 +145,14 @@ impl<'a> Bech32Writer<'a> {
144145
pub fn new(
145146
hrp: &str,
146147
variant: Variant,
148+
checksum: Checksum,
147149
fmt: &'a mut fmt::Write,
148150
) -> Result<Bech32Writer<'a>, fmt::Error> {
149151
let mut writer = Bech32Writer {
150152
formatter: fmt,
151153
chk: 1,
152154
variant,
155+
checksum,
153156
};
154157

155158
writer.formatter.write_str(hrp)?;
@@ -186,6 +189,10 @@ impl<'a> Bech32Writer<'a> {
186189
}
187190

188191
fn inner_finalize(&mut self) -> fmt::Result {
192+
if let Checksum::Exclude = self.checksum {
193+
return Ok(());
194+
}
195+
189196
// Pad with 6 zeros
190197
for _ in 0..6 {
191198
self.polymod_step(u5(0))
@@ -405,13 +412,14 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(
405412
hrp: &str,
406413
data: T,
407414
variant: Variant,
415+
checksum: Checksum,
408416
) -> Result<fmt::Result, Error> {
409417
let hrp_lower = match check_hrp(hrp)? {
410418
Case::Upper => Cow::Owned(hrp.to_lowercase()),
411419
Case::Lower | Case::None => Cow::Borrowed(hrp),
412420
};
413421

414-
match Bech32Writer::new(&hrp_lower, variant, fmt) {
422+
match Bech32Writer::new(&hrp_lower, variant, checksum, fmt) {
415423
Ok(mut writer) => {
416424
Ok(writer.write(data.as_ref()).and_then(|_| {
417425
// Finalize manually to avoid panic on drop if write fails
@@ -452,6 +460,14 @@ impl Variant {
452460
}
453461
}
454462

463+
/// Whether or not a checksum should be included when encoding.
464+
pub enum Checksum {
465+
/// Include a checksum.
466+
Include,
467+
/// Exclude a checksum.
468+
Exclude,
469+
}
470+
455471
/// Encode a bech32 payload to string.
456472
///
457473
/// # Errors
@@ -460,19 +476,48 @@ impl Variant {
460476
/// * No length limits are enforced for the data part
461477
pub fn encode<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<String, Error> {
462478
let mut buf = String::new();
463-
encode_to_fmt(&mut buf, hrp, data, variant)?.unwrap();
479+
encode_to_fmt(&mut buf, hrp, data, variant, Checksum::Include)?.unwrap();
480+
Ok(buf)
481+
}
482+
483+
/// Encode a bech32 payload to string without the checksum.
484+
///
485+
/// # Errors
486+
/// * If [check_hrp] returns an error for the given HRP.
487+
/// # Deviations from standard
488+
/// * No length limits are enforced for the data part
489+
pub fn encode_without_checksum<T: AsRef<[u5]>>(hrp: &str, data: T, variant: Variant) -> Result<String, Error> {
490+
let mut buf = String::new();
491+
encode_to_fmt(&mut buf, hrp, data, variant, Checksum::Exclude)?.unwrap();
464492
Ok(buf)
465493
}
466494

467495
/// Decode a bech32 string into the raw HRP and the data bytes.
468496
///
469-
/// Returns the HRP in lowercase..
497+
/// Returns the HRP in lowercase, the data with the checksum removed, and the encoding.
470498
pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
471-
// Ensure overall length is within bounds
472-
if s.len() < 8 {
499+
let (hrp_lower, mut data) = decode_without_checksum(s)?;
500+
if data.len() < 6 {
473501
return Err(Error::InvalidLength);
474502
}
475503

504+
// Ensure checksum
505+
match verify_checksum(hrp_lower.as_bytes(), &data) {
506+
Some(variant) => {
507+
// Remove checksum from data payload
508+
let dbl: usize = data.len();
509+
data.truncate(dbl - 6);
510+
511+
Ok((hrp_lower, data, variant))
512+
}
513+
None => Err(Error::InvalidChecksum),
514+
}
515+
}
516+
517+
/// Decode a bech32 string into the raw HRP and the data bytes, assuming no checksum.
518+
///
519+
/// Returns the HRP in lowercase and the data with the checksum removed.
520+
pub fn decode_without_checksum(s: &str) -> Result<(String, Vec<u5>), Error> {
476521
// Split at separator and check for two pieces
477522
let (raw_hrp, raw_data) = match s.rfind(SEP) {
478523
None => return Err(Error::MissingSeparator),
@@ -481,9 +526,6 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
481526
(hrp, &data[1..])
482527
}
483528
};
484-
if raw_data.len() < 6 {
485-
return Err(Error::InvalidLength);
486-
}
487529

488530
let mut case = check_hrp(raw_hrp)?;
489531
let hrp_lower = match case {
@@ -493,7 +535,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
493535
};
494536

495537
// Check data payload
496-
let mut data = raw_data
538+
let data = raw_data
497539
.chars()
498540
.map(|c| {
499541
// Only check if c is in the ASCII range, all invalid ASCII
@@ -528,17 +570,7 @@ pub fn decode(s: &str) -> Result<(String, Vec<u5>, Variant), Error> {
528570
})
529571
.collect::<Result<Vec<u5>, Error>>()?;
530572

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-
}
573+
Ok((hrp_lower, data))
542574
}
543575

544576
fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
@@ -792,6 +824,8 @@ mod tests {
792824
Error::InvalidLength),
793825
("1p2gdwpf",
794826
Error::InvalidLength),
827+
("bc1p2",
828+
Error::InvalidLength),
795829
);
796830
for p in pairs {
797831
let (s, expected_error) = p;
@@ -921,13 +955,13 @@ mod tests {
921955
}
922956

923957
#[test]
924-
fn writer() {
958+
fn write_with_checksum() {
925959
let hrp = "lnbc";
926960
let data = "Hello World!".as_bytes().to_base32();
927961

928962
let mut written_str = String::new();
929963
{
930-
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, &mut written_str).unwrap();
964+
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, Checksum::Include, &mut written_str).unwrap();
931965
writer.write(&data).unwrap();
932966
writer.finalize().unwrap();
933967
}
@@ -938,13 +972,30 @@ mod tests {
938972
}
939973

940974
#[test]
941-
fn write_on_drop() {
975+
fn write_without_checksum() {
976+
let hrp = "lnbc";
977+
let data = "Hello World!".as_bytes().to_base32();
978+
979+
let mut written_str = String::new();
980+
{
981+
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, Checksum::Exclude, &mut written_str).unwrap();
982+
writer.write(&data).unwrap();
983+
writer.finalize().unwrap();
984+
}
985+
986+
let encoded_str = encode_without_checksum(hrp, data, Variant::Bech32).unwrap();
987+
988+
assert_eq!(encoded_str, written_str);
989+
}
990+
991+
#[test]
992+
fn write_with_checksum_on_drop() {
942993
let hrp = "lntb";
943994
let data = "Hello World!".as_bytes().to_base32();
944995

945996
let mut written_str = String::new();
946997
{
947-
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, &mut written_str).unwrap();
998+
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, Checksum::Include, &mut written_str).unwrap();
948999
writer.write(&data).unwrap();
9491000
}
9501001

@@ -953,6 +1004,22 @@ mod tests {
9531004
assert_eq!(encoded_str, written_str);
9541005
}
9551006

1007+
#[test]
1008+
fn write_without_checksum_on_drop() {
1009+
let hrp = "lntb";
1010+
let data = "Hello World!".as_bytes().to_base32();
1011+
1012+
let mut written_str = String::new();
1013+
{
1014+
let mut writer = Bech32Writer::new(hrp, Variant::Bech32, Checksum::Exclude, &mut written_str).unwrap();
1015+
writer.write(&data).unwrap();
1016+
}
1017+
1018+
let encoded_str = encode_without_checksum(hrp, data, Variant::Bech32).unwrap();
1019+
1020+
assert_eq!(encoded_str, written_str);
1021+
}
1022+
9561023
#[test]
9571024
fn test_hrp_case() {
9581025
// Tests for issue with HRP case checking being ignored for encoding

0 commit comments

Comments
 (0)