Skip to content

Commit 9f2d02a

Browse files
committed
changed to using ArrayVec in rand to avoid unsafe
1 parent c13557a commit 9f2d02a

3 files changed

Lines changed: 54 additions & 48 deletions

File tree

.github/workflows/test.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,7 @@ jobs:
6565
with:
6666
submodules: true
6767
- uses: dtolnay/rust-toolchain@stable
68-
with:
69-
profile: minimal
7068
- uses: dtolnay/rust-toolchain@nightly
71-
with:
72-
profile: minimal
7369
- uses: taiki-e/install-action@cargo-llvm-cov
7470
- run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json
7571
- uses: codecov/codecov-action@v3

src/lib.rs

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -291,16 +291,18 @@ impl FromStr for Iban {
291291
let ch = characters
292292
.next()
293293
.filter(u8::is_ascii_uppercase)
294+
.map(char::from)
294295
.ok_or(ParseError::CountryCode)?;
295-
iban.push(char::from(ch));
296+
iban.push(ch);
296297
}
297298

298299
for _ in 0..2 {
299300
let ch = characters
300301
.next()
301302
.filter(u8::is_ascii_digit)
303+
.map(char::from)
302304
.ok_or(ParseError::CheckDigit)?;
303-
iban.push(char::from(ch));
305+
iban.push(ch);
304306
}
305307

306308
let country_code = &iban[..2];
@@ -328,11 +330,7 @@ impl FromStr for Iban {
328330
.map_err(|_| ParseError::InvalidLength)?;
329331
}
330332

331-
if validation.next().is_some() {
332-
return Err(ParseError::InvalidLength);
333-
}
334-
335-
if expected_length != iban.len() {
333+
if validation.next().is_some() || expected_length != iban.len() {
336334
return Err(ParseError::InvalidLength);
337335
}
338336

@@ -414,52 +412,40 @@ impl Iban {
414412
country_code: &str,
415413
rng: &mut R,
416414
) -> Result<Self, ParseError> {
417-
let mut iban = ArrayString::<IBAN_MAX_LENGTH>::new();
418-
let mut country_code = country_code.as_bytes().iter().map(u8::to_ascii_uppercase);
415+
use arrayvec::ArrayVec;
419416

420-
for _ in 0..2 {
421-
let ch = country_code
422-
.next()
423-
.filter(u8::is_ascii_uppercase)
424-
.ok_or(ParseError::CountryCode)?;
425-
iban.push(char::from(ch));
426-
}
427-
428-
if country_code.next().is_some() || iban.len() != 2 {
429-
return Err(ParseError::UnknownCountry);
430-
}
417+
use crate::util::array_vec_to_string;
431418

432-
iban.push_str("00");
419+
let mut country_code =
420+
ArrayString::<2>::from_str(country_code).map_err(|_| ParseError::CountryCode)?;
421+
country_code.make_ascii_uppercase();
433422

434423
let &(expected_length, validation, ..) = COUNTRIES
435-
.get(&iban[..2])
424+
.get(&country_code)
436425
.ok_or(ParseError::UnknownCountry)?;
437426

438-
let bban_chars = validation
439-
.iter()
440-
.flat_map(|(count, character_type)| (0..*count).map(move |_| character_type))
441-
.skip(4)
442-
.map(|character_type| char::from(character_type.rand(rng)));
443-
444-
for character in bban_chars {
445-
iban.try_push(character)
446-
.map_err(|_| ParseError::InvalidLength)?;
447-
}
427+
let mut iban = ArrayVec::<u8, IBAN_MAX_LENGTH>::new();
428+
iban.extend(country_code.as_bytes().iter().copied());
429+
iban.extend(b"00".iter().copied());
430+
iban.extend(
431+
validation
432+
.iter()
433+
.flat_map(|(count, character_type)| (0..*count).map(move |_| character_type))
434+
.skip(4)
435+
.map(|character_type| character_type.rand(rng).to_ascii_uppercase()),
436+
);
448437

449438
debug_assert_eq!(iban.len(), expected_length);
450439

451-
let check_digits = 98 - calculate_checksum(iban.as_bytes());
452-
#[allow(clippy::cast_possible_truncation)]
453-
let check_digits = [
454-
b'0' + (check_digits / 10) as u8,
455-
b'0' + (check_digits % 10) as u8,
456-
];
457-
458-
// TODO: Figure out a way to swap out the characters without unsafe.
459-
// SAFETY: All of the characters generated are ASCII, so there are no issues with character boundries.
460-
unsafe { &mut iban.as_bytes_mut()[2..4] }.copy_from_slice(&check_digits);
440+
let check_digits = 98 - calculate_checksum(&iban);
441+
// check_digits is between 2..98, as such dividing by 10 will always give only a single digit.
442+
iban[2..4].copy_from_slice(&[
443+
b'0' + ((check_digits / 10) & 0xFF) as u8,
444+
b'0' + ((check_digits % 10) & 0xFF) as u8,
445+
]);
461446

462-
Ok(Self(iban))
447+
// All characters of iban are ASCII, and as a subset of UTF-8 should always be valid in an ArrayString.
448+
Ok(Self(array_vec_to_string(iban)))
463449
}
464450
}
465451

@@ -854,7 +840,11 @@ mod tests {
854840
// Test that we can construct a random iban for every country
855841
// we support.
856842
for country in crate::COUNTRIES.keys() {
857-
let _ = Iban::rand(country, &mut rng);
843+
let rand_iban = Iban::rand(country, &mut rng).expect("generated iban");
844+
845+
let parsed_iban = Iban::parse(&rand_iban).expect("iban parses");
846+
847+
assert_eq!(rand_iban, parsed_iban);
858848
}
859849
}
860850
}

src/util.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,23 @@ pub fn digits(mut value: u8) -> impl Iterator<Item = u8> {
106106
// Ensure at least one value (0) is provided by this iterator.
107107
.ensure_one(0)
108108
}
109+
110+
/// Converts an `ArrayVec<u8>` to an `ArrayString`.
111+
///
112+
/// # Panics
113+
///
114+
/// This method panics on any `u8` that isn't ASCII.
115+
#[cfg(feature = "rand")]
116+
#[inline]
117+
pub fn array_vec_to_string<const CAP: usize>(
118+
value: arrayvec::ArrayVec<u8, CAP>,
119+
) -> arrayvec::ArrayString<CAP> {
120+
let mut ret = arrayvec::ArrayString::new();
121+
for item in value {
122+
if !item.is_ascii() {
123+
panic!("`{item}` is not an ASCII character.");
124+
}
125+
ret.push(char::from(item));
126+
}
127+
ret
128+
}

0 commit comments

Comments
 (0)