Skip to content

Commit 5f3e951

Browse files
committed
feat: exceptions for base64 errors
Closes #26
1 parent c4796f5 commit 5f3e951

File tree

10 files changed

+340
-190
lines changed

10 files changed

+340
-190
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Dart Package Versioning](https://dart.dev/tools/pub
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- specific exception for base64 decoding errors — [26](https://github.com/dartoos-dev/dartoos/issues/26)
13+
1014
## [0.3.0] - 2021-12-09
1115

1216
### Added

example/encoding/base64_benchmark.dart

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import 'dart:convert';
33
import 'dart:typed_data';
44

5-
import 'package:dartoos/dartoos.dart';
6-
import 'package:dartoos/src/encoding/base64/base64_enc.dart';
5+
import 'package:dartoos/dartoos.dart' as dartoos;
76

87
import '../utils/perf_gain.dart';
98

@@ -27,7 +26,7 @@ void main() {
2726
const len = 50000000;
2827
const alphabet =
2928
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
30-
final bytes = BytesOf.text(Rand(len, alphabet)).value;
29+
final bytes = dartoos.BytesOf.text(dartoos.Rand(len, alphabet)).value;
3130
print('\nLength of the data to be encoded: ${bytes.length} bytes.');
3231

3332
print('\n--- Encoding elapsed times ---');
@@ -39,8 +38,7 @@ void main() {
3938
watch.reset();
4039

4140
watch.start();
42-
const dartoosBase64Enc = Base64Enc();
43-
final dartoosEnc = dartoosBase64Enc(bytes);
41+
final dartoosEnc = dartoos.base64(bytes);
4442
final dartoosEncTime = watch.elapsedMicroseconds / 1000;
4543
print('Dartoos base64-encoding...: $dartoosEncTime milliseconds');
4644
watch.stop();
@@ -57,8 +55,7 @@ void main() {
5755
watch.reset();
5856

5957
watch.start();
60-
const dartoosDecoder = Base64Dec();
61-
final dartoosDec = dartoosDecoder(dartoosEnc);
58+
final dartoosDec = dartoos.base64Dec(dartoosEnc);
6259
final dartoosDecTime = watch.elapsedMicroseconds / 1000;
6360
print('Dartoos base64-decoding...: $dartoosDecTime milliseconds');
6461
watch.stop();
@@ -80,13 +77,3 @@ bool bytesEqual(Uint8List first, Uint8List second) {
8077
}
8178
return i == length;
8279
}
83-
84-
/// Returns a formatted string describing the performance gain ('+', positive
85-
/// values) or loss ('-', negative values).
86-
///
87-
/// Examples: +12% for a performance gain; -5.5% for a performance loss.
88-
// String perfGain(double dartTime, double dartoosTime) {
89-
// final perc = ((dartTime / dartoosTime) * 100) - 100;
90-
// final sign = perc > 0 ? '+' : '';
91-
// return "$sign${perc.toStringAsFixed(2)}% ('+' gain; '-' loss)";
92-
// }

lib/base64.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
/// >
88
/// > — [Base64 encoding. In Wikipedia, The Free
99
/// > Encyclopedia](https://en.wikipedia.org/w/index.php?title=Base64&oldid=1054311270)
10+
///
11+
/// Specification:
12+
///
13+
/// - [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648)
1014
library base64;
1115

1216
export 'src/encoding/base64/base64.dart';
1317
export 'src/encoding/base64/base64_dec.dart';
18+
export 'src/encoding/base64/base64_enc.dart';
1419
export 'src/encoding/base64/base64_norm.dart';

lib/src/encoding/base64/base64.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,101 @@ abstract class Base64Decoder implements BinToTextDec {
2424
@override
2525
Uint8List call(String encoded);
2626
}
27+
28+
/// Thrown when trying to decode strings that do not have a proper base64
29+
/// format.
30+
class Base64Exception extends FormatException {
31+
/// Creates a `Base64FormatException` with an error [message].
32+
///
33+
/// Optionally, you can also supply the [source] with the incorrect format and
34+
/// the [offset] where the error was detected.
35+
const Base64Exception(
36+
this.errorType,
37+
String message, [
38+
dynamic source,
39+
int? offset,
40+
]) : super(message, source, offset);
41+
42+
/// Invalid base64-encoded text length
43+
///
44+
/// Optionally, you can also supply the [source] with the incorrect format and
45+
/// the [offset] where the error was detected.
46+
const Base64Exception.length({
47+
String message = 'Invalid base64 length.',
48+
dynamic source,
49+
int? offset,
50+
}) : this(Base64FormatError.length, message, source, offset);
51+
52+
/// Invalid percent-encoding of '=' — Base64Url.
53+
///
54+
/// Optionally, you can also supply the [source] with the incorrect format and
55+
/// the [offset] where the error was detected.
56+
const Base64Exception.percEnc({
57+
String message = "Base64 invalid percent-encoding of '='.",
58+
dynamic source,
59+
int? offset,
60+
}) : this(Base64FormatError.percEnc, message, source, offset);
61+
62+
/// Encoded text contains non-ascii character(s).
63+
///
64+
/// Optionally, you can also supply the [source] with the incorrect format and
65+
/// the [offset] where the error was detected.
66+
const Base64Exception.nonAscii({
67+
String message = 'Base64-encoded text contains non-ascii character(s).',
68+
dynamic source,
69+
int? offset,
70+
}) : this(Base64FormatError.nonAscii, message, source, offset);
71+
72+
/// Encoded text conains illegal ascii symbol(s).
73+
///
74+
/// Optionally, you can also supply the [source] with the incorrect format and
75+
/// the [offset] where the error was detected.
76+
const Base64Exception.illegalAscii({
77+
String message =
78+
"Base64-encoded text contains illegal ascii symbol(s) (characters other than 'A–Za–z0–9+-/_').",
79+
dynamic source,
80+
int? offset,
81+
}) : this(Base64FormatError.illegalAscii, message, source, offset);
82+
83+
/// Invalid base64-encoded padding.
84+
///
85+
/// Optionally, you can also supply the [source] with the incorrect format and
86+
/// the [offset] where the error was detected.
87+
const Base64Exception.padding({
88+
String message = "Base64 invalid padding",
89+
dynamic source,
90+
int? offset,
91+
}) : this(Base64FormatError.padding, message, source, offset);
92+
93+
/// The actual type of the format error.
94+
final Base64FormatError errorType;
95+
}
96+
97+
/// Specific error types to be used along with [Base64Exception].
98+
///
99+
/// Useful for fine-grained error handling.
100+
enum Base64FormatError {
101+
/// Invalid base64-encoded text length
102+
length,
103+
104+
/// Invalid percent-encoding of trailing '=' — Base64Url
105+
percEnc,
106+
107+
/// Encoded text contains non-ascii character(s)
108+
nonAscii,
109+
110+
/// Encoded text conains an illegal ascii symbol
111+
///
112+
/// Any character other than 'A-Za-z0-9+-/_' have been found.
113+
illegalAscii,
114+
115+
/// Invalid padding
116+
///
117+
/// Examples:
118+
///
119+
/// - unnecessary trailing '='.
120+
/// - missing '==' at the end of the encoded text.
121+
/// - there should be a single '=' padding character but multiple '=' were
122+
/// found.
123+
padding
124+
}

lib/src/encoding/base64/base64_dec.dart

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,35 @@ import '../../byte.dart';
44
import 'base64.dart';
55
import 'base64_norm.dart';
66

7+
/// Strict Base64 Decoder
8+
///
9+
/// **alphabet**: A–Za–z0–9+/
10+
///
11+
/// It requires padding and throws [Base64Exception] if the incoming text is
12+
/// ill-formed. For a more relaxed decoder, see [base64DecNorm].
13+
///
14+
/// Base64 encoding scheme —
15+
/// [RFC 4648 section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
16+
const base64Dec = Base64Dec();
17+
18+
/// Normalizing Base64 Decoder
19+
///
20+
/// It normalizes the encoded text before trying to decode it.
21+
///
22+
/// Normalization process:
23+
///
24+
/// - Unescape any '%' that preceeds '3D' (percent-encoded '=').
25+
/// - Replace '_' or '-' with '/' or '+'.
26+
/// - Add trailing padding '=' if needed.
27+
/// - Only base64 characters (A–Za–z0–9/+).
28+
/// - The total length will always be a multiple of four.
29+
const base64DecNorm = Base64Dec.norm();
30+
731
/// Base64 Decoder.
832
///
33+
/// Throws [Base64Exception] when [encoded] does not have the expected base64
34+
/// format and cannot be parsed.
35+
///
936
/// Base64 encoding scheme —
1037
/// [RFC 4648 section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
1138
class Base64Dec implements Base64Decoder {
@@ -30,31 +57,32 @@ class Base64Dec implements Base64Decoder {
3057
/// - Add trailing padding '=' if needed.
3158
/// - Only base64 characters (A–Za–z0–9/+).
3259
/// - The total length will always be a multiple of four.
33-
const Base64Dec.norm() : this(const Base64Norm());
60+
const Base64Dec.norm() : this(base64Norm);
3461

3562
// Restores the original bytes.
3663
final _ProperlyPadded _decode;
3764

3865
// The normalization phase.
3966
final Base64NormText _norm;
4067

68+
/// Returns the decoded bytes of [encoded].
69+
///
70+
/// Throws [Base64Exception] when [encoded] does not have the expected base64
71+
/// format and cannot be parsed.
4172
@override
4273
Uint8List call(String encoded) => _decode(_norm(encoded));
4374
}
4475

4576
/// Convenience Base64 decoder implementation over [Base64Dec].
4677
class Base64DecOf implements Bytes {
4778
/// Decoded bytes of [encoded] text.
48-
const Base64DecOf(String encoded, [Base64Dec dec = const Base64Dec()])
49-
: _encoded = encoded,
50-
_dec = dec;
79+
const Base64DecOf(String encoded) : this._set(encoded, base64Dec);
5180

5281
/// Normalizes [encoded] before trying to decode it.
53-
const Base64DecOf.norm(String encoded)
54-
: this(encoded, const Base64Dec.norm());
82+
const Base64DecOf.norm(String encoded) : this._set(encoded, base64DecNorm);
5583

56-
/// Custom
57-
const Base64DecOf.custom(this._encoded, this._dec);
84+
/// Sets the encoded text and decoder instance.
85+
const Base64DecOf._set(this._encoded, this._dec);
5886

5987
final String _encoded;
6088
final Base64Dec _dec;
@@ -95,32 +123,38 @@ class _ProperlyPadded {
95123
Uint8List call(String base64) {
96124
final info = _Base64Info(base64);
97125
if (info.totalLength % 4 != 0) {
98-
throw const FormatException('Invalid base64 encoding length');
126+
throw const Base64Exception.length(
127+
message: 'Base64 encoding length must be a multiple of 4.',
128+
);
99129
}
100130
switch (info.payloadLength % 4) {
101131
case 1:
102-
throw const FormatException('Invalid base64 payload length');
132+
throw const Base64Exception.length(
133+
message: 'Invalid base64 payload length',
134+
);
103135
case 0:
104136
// There must be no padding.
105137
if (info.numOfPadChars != 0) {
106-
throw const FormatException(
107-
"Invalid base64 padding: unnecessary trailing '='.",
138+
throw const Base64Exception.padding(
139+
message: "Invalid base64 padding: unnecessary trailing '='.",
108140
);
109141
}
110142
break;
111143
case 2:
112144
// There must be exactly 2 padding chars.
113145
if (info.numOfPadChars != 2) {
114-
throw const FormatException(
115-
"Invalid base64 padding: missing '==' at the end of the encoded text.",
146+
throw const Base64Exception.padding(
147+
message:
148+
"Invalid base64 padding: missing '==' at the end of the encoded text.",
116149
);
117150
}
118151
break;
119152
case 3:
120153
// There must be a single padding char at the end.
121154
if (info.numOfPadChars != 1) {
122-
throw const FormatException(
123-
"Invalid base64 padding: there should only be one '=' at the end of the encoded text.",
155+
throw const Base64Exception.padding(
156+
message:
157+
"Invalid base64 padding: there should be a single '=' at the end of the encoded text.",
124158
);
125159
}
126160
}
@@ -192,15 +226,11 @@ class _Base64Indexes {
192226
int operator [](int i) {
193227
final code = _base64.codeUnitAt(i);
194228
if (code < 0 || code > 127) {
195-
throw const FormatException(
196-
'Illegal base64 character: non-ascii character found.',
197-
);
229+
throw const Base64Exception.nonAscii();
198230
}
199231
final index = _asciiBase64Indexes[code];
200232
if (index < 0) {
201-
throw const FormatException(
202-
"Invalid base64 character: ascii character other than '[A-Za-z][0-9]+/'.",
203-
);
233+
throw const Base64Exception.illegalAscii();
204234
}
205235
return index;
206236
}

lib/src/encoding/base64/base64_enc.dart

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,47 @@ import 'dart:typed_data';
22

33
import 'base64.dart';
44

5+
/// Standard base64 encoding.
6+
///
7+
/// **alphabet**: A–Za–z0–9+/
8+
/// **padding**: '=' or '==', if needed.
9+
///
10+
/// [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
11+
const base64 = Base64Enc();
12+
13+
/// Standard base64 encoding without padding '=' signs.
14+
///
15+
/// **alphabet**: A–Za–z0–9+/
16+
///
17+
/// [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
18+
const base64NoPad = Base64Enc.noPad();
19+
20+
/// URL- and filename-safe base64 encoding.
21+
///
22+
/// **alphabet**: A–Za–z0–9-_
23+
/// **padding**: '=' or '==', if needed.
24+
///
25+
/// [Base64Url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
26+
const base64Url = Base64UrlEnc();
27+
28+
/// URL- and filename-safe base64 encoding without padding '=' signs
29+
///
30+
/// **alphabet**: A–Za–z0–9-_
31+
///
32+
/// [Base64Url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
33+
const base64UrlNoPad = Base64UrlEnc.noPad();
34+
535
/// Standard Base64 Encoder
636
///
737
/// [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)
838
class Base64Enc implements Base64Encoder {
9-
/// Base64.
39+
/// Base64 Encoding
1040
///
1141
/// **alphabet**: A–Za–z0–9+/
12-
/// **padding**: '='
42+
/// **padding**: '=' or '==', if needed.
1343
const Base64Enc() : this._set(const _Base64Str(_Pad(_Base64Indexes(_std))));
1444

15-
/// Base64 without padding '=' signs.
45+
/// Base64 encoding without padding '=' signs
1646
///
1747
/// **alphabet**: A–Za–z0–9+/
1848
const Base64Enc.noPad()
@@ -38,14 +68,14 @@ class Base64Enc implements Base64Encoder {
3868
///
3969
/// [Base64Url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
4070
class Base64UrlEnc implements Base64Encoder {
41-
/// Base64Url
71+
/// Base64Url Encoding
4272
///
4373
/// **default alphabet**: A–Za–z0–9-_
44-
/// **padding**: '='.
74+
/// **padding**: '=' or '==', if needed.
4575
const Base64UrlEnc()
4676
: this._set(const _Base64Str(_Pad(_Base64Indexes(_url))));
4777

48-
/// Base64Url without padding '=' signs.
78+
/// Base64Url encoding without padding '=' signs
4979
///
5080
/// **alphabet**: A–Za–z0–9-_
5181
const Base64UrlEnc.noPad()

0 commit comments

Comments
 (0)