Skip to content

Commit f2c1728

Browse files
authored
v0.12: Further deprecate base64 byte strings by removing parsing support outside the error path (#566)
* Further deprecate base64 byte strings by removing parsing support outside the error path * Remove non-test base64 dependency * Update the CHANGELOG
1 parent 5630b0f commit f2c1728

File tree

5 files changed

+105
-128
lines changed

5 files changed

+105
-128
lines changed

CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9-
### Additions
9+
### API Changes
1010

11+
- Breaking: Removed the `ron::error::Error::Base64Error` variant. ([#566](https://github.com/ron-rs/ron/pull/566))
1112
- Added `into_inner()` method to `ron::ser::Serializer` to retrieve the inner writer. ([#588](https://github.com/ron-rs/ron/pull/588))
13+
- Removed the `base64` dependency. ([#566](https://github.com/ron-rs/ron/pull/566))
14+
15+
### Format Changes
16+
17+
- **Format-Breaking:** Remote base64-encoded byte strings deserialisation, replaced by Rusty byte strings in v0.9.0 ([#566](https://github.com/ron-rs/ron/pull/566))
1218

1319
### Bug Fixes
1420

@@ -19,9 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1925
### API Changes
2026

2127
- Breaking: `SpannedError` now stores the full error span in span: Span { start: Position, end: Position }`, to facilitate, e.g., language server highlighting of syntax errors.
22-
2328
- Breaking: Added `no_std` support via a new `std` feature (enabled by default). With default features disabled, you must enable the `std` feature to access `de::from_reader`, and the `std::io` operations on `Options`, such as `from_reader`, `from_reader_seed`, `to_io_writer`, and `to_io_writer_pretty` ([#567](https://github.com/ron-rs/ron/pull/567))
24-
2529
- Breaking: Fixed (again) `ron::value::Number` to ensure it is non-exhaustive, to avoid breaking `match`es when feature unification enables more of its variants than expected ([#568](https://github.com/ron-rs/ron/pull/568))
2630

2731
### Examples

Cargo.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,27 @@ indexmap = ["std", "dep:indexmap"]
2828
internal-span-substring-test = ["unicode-segmentation"]
2929

3030
[dependencies]
31-
# FIXME @juntyr remove base64 once old byte strings are fully deprecated
32-
base64 = { version = "0.22", default-features = false, features = ["alloc"] }
3331
bitflags = { version = "2.1", default-features = false, features = ["serde"] }
3432
indexmap = { version = "2.0", default-features = false, features = ["serde"], optional = true }
33+
once_cell = { version = "1.20", default-features = false, features = ["alloc", "race"] }
3534
# serde supports i128/u128 from 1.0.60 onwards
3635
# serde's IntoDeserializer impls suport new constructor from 1.0.139 onwards
3736
# serde's adjacently tagged enums support integer tags from 1.0.181 onwards
3837
serde = { version = "1.0.181", default-features = false, features = ["alloc"] }
3938
serde_derive = { version = "1.0.181", default-features = false }
40-
unicode-ident = { version = "1.0", default-features = false }
41-
unicode-segmentation = { version = "1.12.0", optional = true, default-features = false }
4239
typeid = { version = "1.0.1", default-features = false }
43-
once_cell = { version = "1.20", default-features = false, features = ["alloc", "race"] }
40+
unicode-ident = { version = "1.0", default-features = false }
41+
unicode-segmentation = { version = "1.12.0", default-features = false, optional = true }
4442

4543
[dev-dependencies]
44+
base64 = { version = "0.22", default-features = false, features = ["std"] }
45+
bytes = { version = "1.3", default-features = false, features = ["serde"] }
46+
option_set = { version = "0.3", default-features = false }
4647
serde = { version = "1.0.181", default-features = false, features = ["std", "derive"] }
4748
serde_bytes = { version = "0.11", default-features = false, features = ["std"] }
4849
# serde_json supports the std feature from 1.0.60 onwards
4950
serde_json = { version = "1.0.60", default-features = false, features = ["std"] }
50-
option_set = { version = "0.3", default-features = false }
5151
typetag = { version = "0.2", default-features = false }
52-
bytes = { version = "1.3", default-features = false, features = ["serde"] }
5352

5453
[package.metadata.docs.rs]
5554
features = ["integer128", "indexmap"]

src/error.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ pub enum Error {
3333
Fmt,
3434
Io(String),
3535
Message(String),
36-
#[deprecated(
37-
since = "0.9.0",
38-
note = "ambiguous base64 byte strings are replaced by strongly typed Rusty b\"byte strings\""
39-
)]
40-
Base64Error(base64::DecodeError),
4136
Eof,
4237
ExpectedArray,
4338
ExpectedArrayEnd,
@@ -131,8 +126,6 @@ impl fmt::Display for Error {
131126
match *self {
132127
Error::Fmt => f.write_str("Formatting RON failed"),
133128
Error::Io(ref s) | Error::Message(ref s) => f.write_str(s),
134-
#[allow(deprecated)]
135-
Error::Base64Error(ref e) => write!(f, "Invalid base64: {}", e),
136129
Error::Eof => f.write_str("Unexpected end of RON"),
137130
Error::ExpectedArray => f.write_str("Expected opening `[`"),
138131
Error::ExpectedArrayEnd => f.write_str("Expected closing `]`"),
@@ -530,11 +523,6 @@ mod tests {
530523
);
531524
check_error_message(&<Error as SerError>::custom("my-ser-error"), "my-ser-error");
532525
check_error_message(&<Error as DeError>::custom("my-de-error"), "my-de-error");
533-
#[allow(deprecated)]
534-
check_error_message(
535-
&Error::Base64Error(base64::DecodeError::InvalidPadding),
536-
"Invalid base64: Invalid padding",
537-
);
538526
check_error_message(&Error::Eof, "Unexpected end of RON");
539527
check_error_message(&Error::ExpectedArray, "Expected opening `[`");
540528
check_error_message(&Error::ExpectedArrayEnd, "Expected closing `]`");

src/parse.rs

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,35 +1039,23 @@ impl<'a> Parser<'a> {
10391039
}
10401040
}
10411041

1042+
// FIXME @juntyr: remove in v0.13, since only byte_string_no_base64 will
1043+
// be used
10421044
if self.consume_char('"') {
10431045
let base64_str = self.escaped_string()?;
10441046
let base64_result = ParsedByteStr::try_from_base64(&base64_str);
10451047

1046-
if cfg!(not(test)) {
1047-
// FIXME @juntyr: remove in v0.12
1048-
#[allow(deprecated)]
1049-
base64_result.map_err(Error::Base64Error)
1050-
} else {
1051-
match base64_result {
1052-
// FIXME @juntyr: enable in v0.12
1053-
Ok(byte_str) => Err(expected_byte_string_found_base64(&base64_str, &byte_str)),
1054-
Err(_) => Err(Error::ExpectedByteString),
1055-
}
1048+
match base64_result {
1049+
Some(byte_str) => Err(expected_byte_string_found_base64(&base64_str, &byte_str)),
1050+
None => Err(Error::ExpectedByteString),
10561051
}
10571052
} else if self.consume_char('r') {
10581053
let base64_str = self.raw_string()?;
10591054
let base64_result = ParsedByteStr::try_from_base64(&base64_str);
10601055

1061-
if cfg!(not(test)) {
1062-
// FIXME @juntyr: remove in v0.12
1063-
#[allow(deprecated)]
1064-
base64_result.map_err(Error::Base64Error)
1065-
} else {
1066-
match base64_result {
1067-
// FIXME @juntyr: enable in v0.12
1068-
Ok(byte_str) => Err(expected_byte_string_found_base64(&base64_str, &byte_str)),
1069-
Err(_) => Err(Error::ExpectedByteString),
1070-
}
1056+
match base64_result {
1057+
Some(byte_str) => Err(expected_byte_string_found_base64(&base64_str, &byte_str)),
1058+
None => Err(Error::ExpectedByteString),
10711059
}
10721060
} else {
10731061
self.byte_string_no_base64()
@@ -1641,14 +1629,75 @@ impl<'a> ParsedStr<'a> {
16411629
}
16421630

16431631
impl<'a> ParsedByteStr<'a> {
1644-
pub fn try_from_base64(str: &ParsedStr<'a>) -> Result<Self, base64::DecodeError> {
1632+
pub fn try_from_base64(str: &ParsedStr<'a>) -> Option<Self> {
1633+
// Adapted from MIT licensed Jenin Sutradhar's base 64 decoder
1634+
// https://github.com/JeninSutradhar/base64-Rust-Encoder-Decoder/blob/ee1fb08cbb78024ec8cf5e786815acb239169f02/src/lib.rs#L84-L128
1635+
fn try_decode_base64(str: &str) -> Option<Vec<u8>> {
1636+
const CHARSET: &[u8; 64] =
1637+
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1638+
const PADDING: u8 = b'=';
1639+
1640+
// fast reject for missing padding
1641+
if (str.len() % 4) != 0 {
1642+
return None;
1643+
}
1644+
1645+
let bstr_no_padding = str.trim_end_matches(char::from(PADDING)).as_bytes();
1646+
1647+
// fast reject for excessive padding
1648+
if (str.len() - bstr_no_padding.len()) > 2 {
1649+
return None;
1650+
}
1651+
1652+
// fast reject for extraneous bytes after padding
1653+
if bstr_no_padding.contains(&PADDING) {
1654+
return None;
1655+
}
1656+
1657+
// fast reject for non-ASCII
1658+
if !str.is_ascii() {
1659+
return None;
1660+
}
1661+
1662+
let mut collected_bits = 0_u8;
1663+
let mut byte_buffer = 0_u16;
1664+
let mut bytes = bstr_no_padding.iter().copied();
1665+
let mut binary = Vec::new();
1666+
1667+
'decodeloop: loop {
1668+
while collected_bits < 8 {
1669+
if let Some(nextbyte) = bytes.next() {
1670+
#[allow(clippy::cast_possible_truncation)]
1671+
if let Some(idx) = CHARSET.iter().position(|&x| x == nextbyte) {
1672+
byte_buffer |= ((idx & 0b0011_1111) as u16) << (10 - collected_bits);
1673+
collected_bits += 6;
1674+
} else {
1675+
return None;
1676+
}
1677+
} else {
1678+
break 'decodeloop;
1679+
}
1680+
}
1681+
1682+
binary.push(((0b1111_1111_0000_0000 & byte_buffer) >> 8) as u8);
1683+
byte_buffer &= 0b0000_0000_1111_1111;
1684+
byte_buffer <<= 8;
1685+
collected_bits -= 8;
1686+
}
1687+
1688+
if usize::from(collected_bits) != ((str.len() - bstr_no_padding.len()) * 2) {
1689+
return None;
1690+
}
1691+
1692+
Some(binary)
1693+
}
1694+
16451695
let base64_str = match str {
16461696
ParsedStr::Allocated(string) => string.as_str(),
16471697
ParsedStr::Slice(str) => str,
16481698
};
16491699

1650-
base64::engine::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_str)
1651-
.map(ParsedByteStr::Allocated)
1700+
try_decode_base64(base64_str).map(ParsedByteStr::Allocated)
16521701
}
16531702
}
16541703

@@ -1786,7 +1835,7 @@ mod tests {
17861835
}
17871836

17881837
#[test]
1789-
fn v0_10_base64_deprecation_error() {
1838+
fn base64_deprecation_error() {
17901839
let err = crate::from_str::<bytes::Bytes>("\"SGVsbG8gcm9uIQ==\"").unwrap_err();
17911840

17921841
assert_eq!(
@@ -1810,7 +1859,10 @@ mod tests {
18101859
assert_eq!(
18111860
crate::from_str::<bytes::Bytes>("\"invalid=\"").unwrap_err(),
18121861
SpannedError {
1813-
code: Error::ExpectedByteString,
1862+
code: Error::InvalidValueForType {
1863+
expected: String::from("the Rusty byte string b\"\\x8a{\\xda\\x96\\'\""),
1864+
found: String::from("the ambiguous base64 string \"invalid=\"")
1865+
},
18141866
span: Span {
18151867
start: Position { line: 1, col: 2 },
18161868
end: Position { line: 1, col: 11 },
@@ -1821,12 +1873,26 @@ mod tests {
18211873
assert_eq!(
18221874
crate::from_str::<bytes::Bytes>("r\"invalid=\"").unwrap_err(),
18231875
SpannedError {
1824-
code: Error::ExpectedByteString,
1876+
code: Error::InvalidValueForType {
1877+
expected: String::from("the Rusty byte string b\"\\x8a{\\xda\\x96\\'\""),
1878+
found: String::from("the ambiguous base64 string \"invalid=\"")
1879+
},
18251880
span: Span {
18261881
start: Position { line: 1, col: 3 },
18271882
end: Position { line: 1, col: 12 },
18281883
}
18291884
}
18301885
);
1886+
1887+
assert_eq!(
1888+
crate::from_str::<bytes::Bytes>("r\"invalid\"").unwrap_err(),
1889+
SpannedError {
1890+
code: Error::ExpectedByteString,
1891+
span: Span {
1892+
start: Position { line: 1, col: 3 },
1893+
end: Position { line: 1, col: 11 },
1894+
}
1895+
}
1896+
);
18311897
}
18321898
}

tests/438_rusty_byte_strings.rs

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,86 +11,6 @@ struct BytesStruct {
1111
large: Vec<u8>,
1212
}
1313

14-
#[test]
15-
fn v_9_deprecated_base64_bytes_support() {
16-
#![allow(deprecated)]
17-
18-
// Requires padding of the base64 data
19-
assert_eq!(
20-
Ok(BytesStruct {
21-
small: vec![1, 2],
22-
large: vec![1, 2, 3, 4]
23-
}),
24-
ron::from_str("BytesStruct( small:[1, 2], large:\"AQIDBA==\" )"),
25-
);
26-
27-
// Requires no padding of the base64 data
28-
assert_eq!(
29-
Ok(BytesStruct {
30-
small: vec![1, 2],
31-
large: vec![1, 2, 3, 4, 5, 6]
32-
}),
33-
ron::from_str("BytesStruct( small:[1, 2], large:r\"AQIDBAUG\" )"),
34-
);
35-
36-
// Parsing an escaped byte string is also possible
37-
assert_eq!(
38-
Ok(BytesStruct {
39-
small: vec![1, 2],
40-
large: vec![1, 2, 3, 4, 5, 6]
41-
}),
42-
ron::from_str("BytesStruct( small:[1, 2], large:\"\\x41Q\\x49DBA\\x55G\" )"),
43-
);
44-
45-
// Invalid base64
46-
assert_eq!(
47-
Err(SpannedError {
48-
code: Error::Base64Error(base64::DecodeError::InvalidByte(0, b'_')),
49-
span: Span {
50-
start: Position { line: 1, col: 35 },
51-
end: Position { line: 1, col: 40 }
52-
}
53-
}),
54-
ron::from_str::<BytesStruct>("BytesStruct( small:[1, 2], large:\"_+!!\" )"),
55-
);
56-
57-
// Invalid last base64 symbol
58-
assert_eq!(
59-
Err(SpannedError {
60-
code: Error::Base64Error(base64::DecodeError::InvalidLastSymbol(1, b'x')),
61-
span: Span {
62-
start: Position { line: 1, col: 35 },
63-
end: Position { line: 1, col: 40 }
64-
}
65-
}),
66-
ron::from_str::<BytesStruct>("BytesStruct( small:[1, 2], large:\"/x==\" )"),
67-
);
68-
69-
// Missing padding
70-
assert_eq!(
71-
Err(SpannedError {
72-
code: Error::Base64Error(base64::DecodeError::InvalidPadding),
73-
span: Span {
74-
start: Position { line: 1, col: 35 },
75-
end: Position { line: 1, col: 42 }
76-
}
77-
}),
78-
ron::from_str::<BytesStruct>("BytesStruct( small:[1, 2], large:\"AQIDBA\" )"),
79-
);
80-
81-
// Too much padding
82-
assert_eq!(
83-
Err(SpannedError {
84-
code: Error::Base64Error(base64::DecodeError::InvalidByte(6, b'=')),
85-
span: Span {
86-
start: Position { line: 1, col: 35 },
87-
end: Position { line: 1, col: 45 }
88-
}
89-
}),
90-
ron::from_str::<BytesStruct>("BytesStruct( small:[1, 2], large:\"AQIDBA===\" )"),
91-
);
92-
}
93-
9414
#[test]
9515
fn rusty_byte_string() {
9616
assert_eq!(

0 commit comments

Comments
 (0)