-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathlib.rs
368 lines (333 loc) · 13.1 KB
/
lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
// Copyright © 2022 Translucence Research, Inc. All rights reserved.
//! User-oriented format for binary data. Tagged Base64 is intended to
//! be used in user interfaces including URLs and text to be copied
//! and pasted without the need for additional encoding, such as
//! quoting or escape sequences. A checksum is included so that common
//! problems such as inadvertent deletions or typos can be caught
//! without knowing the structure of the binary data.
//!
//! To further reduce confusion, the values are prefixed with a tag
//! intended to disambiguate usage. Although not necessary for
//! correctness, developers and users may find it convenient to have a
//! usage hint enabling them to see at a glance whether something is a
//! transaction id or a ledger address, etc.
//!
//! For example,
//! ```text
//! KEY~cHVibGljIGtleSBiaXRzBQ
//! TX~dHJhbnNhY3Rpb24gaWRlbnRpZmllciBnb2VzIGhlcmUC
//! Zg~Zgg
//! mytag~bXl0YWd7
//! ```
//!
//! Like the base64 value, the tag is also restricted to the URL-safe
//! base64 character set.
//!
//! Note: It is allowed for the tag to be the empty string. The base64
//! portion cannot be empty; at a minimum, it will encode a single
//! byte checksum.
//!
//! The tag and delimiter help to avoid problems with binary values
//! that happen to parse as numbers. Large binary values don't fit
//! nicely into JavaScript numbers due to range and
//! representation. JavaScript numbers are represented as 64-bit
//! floating point numbers. This means that the largest unsigned
//! integer that can be represented is 2^53 - 1. Moreover, it is very
//! easy to accidentally coerce a string that looks like a number into
//! a JavaScript number, thus running the risk of loss of precision,
//! which is corruption. Therefore, values are encoded in base64 to
//! allow safe transit to- and from JavaScript, including in URLs, as
//! well as display and input in a user interface.
#![allow(clippy::unused_unit)]
use core::fmt;
use core::fmt::Display;
use crc_any::CRC;
use wasm_bindgen::prelude::*;
/// Separator that does not appear in URL-safe base64 encoding and can
/// appear in URLs without percent-encoding.
pub const TB64_DELIM: char = '~';
/// Uses '-' and '_' as the 63rd and 64th characters. Does not use padding.
pub const TB64_CONFIG: base64::Config = base64::URL_SAFE_NO_PAD;
/// A structure holding a string tag, vector of bytes, and a checksum
/// covering the tag and the bytes.
#[wasm_bindgen]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TaggedBase64 {
tag: String,
value: Vec<u8>,
checksum: u8,
}
/// JavaScript-compatible wrapper for TaggedBase64
///
/// The primary difference is that JsTaggedBase64 returns errors
/// of type JsValue.
#[wasm_bindgen]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JsTaggedBase64 {
tb64: TaggedBase64,
}
#[derive(Debug)]
pub enum Tb64Error {
/// An invalid character was found in the tag.
InvalidTag,
/// Missing delimiter.
MissingDelimiter,
/// Missing checksum in value.
MissingChecksum,
/// An invalid byte was found while decoding the base64-encoded value.
/// The offset and offending byte are provided.
InvalidByte(usize, u8),
/// The last non-padding input symbol's encoded 6 bits have
/// nonzero bits that will be discarded. This is indicative of
/// corrupted or truncated Base64. Unlike InvalidByte, which
/// reports symbols that aren't in the alphabet, this error is for
/// symbols that are in the alphabet but represent nonsensical
/// encodings.
InvalidLastSymbol(usize, u8),
/// The length of the base64-encoded value is invalid.
InvalidLength,
/// The checksum was truncated or did not match.
InvalidChecksum,
}
impl fmt::Display for Tb64Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Tb64Error::InvalidTag =>
write!(f, "An invalid character was found in the tag."),
Tb64Error::MissingDelimiter =>
write!(f, "Missing delimiter ({}).", TB64_DELIM),
Tb64Error::MissingChecksum =>
write!(f, "Missing checksum in value."),
Tb64Error::InvalidByte(offset, byte) =>
write!(f, "An invalid byte ({:#0x}) was found at offset {} while decoding the base64-encoded value. The offset and offending byte are provided.", byte, offset),
Tb64Error::InvalidLastSymbol(offset, byte) => write!(f, "The last non-padding input symbol's encoded 6 bits have nonzero bits that will be discarded. This is indicative of corrupted or truncated Base64. Unlike InvalidByte, which reports symbols that aren't in the alphabet, this error is for symbols that are in the alphabet but represent nonsensical encodings. Invalid byte ({:#0x}) at offset {}.", byte, offset),
Tb64Error::InvalidLength =>
write!(f, "The length of the base64-encoded value is invalid."),
Tb64Error::InvalidChecksum =>
write!(f, "The checksum was truncated or did not match."),
}
}
}
/// Converts a TaggedBase64 value to a String.
#[wasm_bindgen]
pub fn to_string(tb64: &TaggedBase64) -> String {
let value = &mut tb64.value.clone();
value.push(TaggedBase64::calc_checksum(&tb64.tag, &tb64.value));
format!(
"{}{}{}",
tb64.tag,
TB64_DELIM,
TaggedBase64::encode_raw(value)
)
}
impl From<&TaggedBase64> for String {
fn from(tb64: &TaggedBase64) -> Self {
let value = &mut tb64.value.clone();
value.push(TaggedBase64::calc_checksum(&tb64.tag, &tb64.value));
format!(
"{}{}{}",
tb64.tag,
TB64_DELIM,
TaggedBase64::encode_raw(value)
)
}
}
/// Produces the string of a TaggedBase64 value by concatenating the
/// tag, a delimeter, and the base64 encoding of the value and
/// checksum.
impl fmt::Display for TaggedBase64 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = &mut self.value.clone();
value.push(TaggedBase64::calc_checksum(&self.tag, &self.value));
write!(
f,
"{}{}{}",
self.tag,
TB64_DELIM,
TaggedBase64::encode_raw(value)
)
}
}
/// Produces the string of a TaggedBase64 value by concatenating the
/// tag, a delimeter, and the base64 encoding of the value and
/// checksum.
impl fmt::Display for JsTaggedBase64 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.tb64)
}
}
impl PartialEq<TaggedBase64> for JsTaggedBase64 {
fn eq(&self, other: &TaggedBase64) -> bool {
self.tb64 == *other
}
}
impl TaggedBase64 {
/// Constructs a TaggedBase64 from a tag and array of bytes. The tag
/// must be URL-safe (alphanumeric with hyphen and underscore). The
/// byte values are unconstrained.
/// ```ignored
/// use TaggedBase64;
/// let tb64 = TaggedBase64::new("TAG-YOURE-IT", b"datadatadata");
/// ```
pub fn new(tag: &str, value: &[u8]) -> Result<TaggedBase64, Tb64Error> {
if TaggedBase64::is_safe_base64_tag(tag) {
let cs = TaggedBase64::calc_checksum(tag, value);
Ok(TaggedBase64 {
tag: tag.to_string(),
value: value.to_vec(),
checksum: cs,
})
} else {
Err(Tb64Error::InvalidTag)
}
}
/// Parses a string of the form tag~value into a TaggedBase64 value.
///
/// The tag is restricted to URL-safe base64 ASCII characters. The tag
/// may be empty. The delimiter is required.
///
/// The value is a base64-encoded string, using the URL-safe character
/// set, and no padding is used.
pub fn parse(tb64: &str) -> Result<TaggedBase64, Tb64Error> {
// Would be convenient to use split_first() here. Alas, not stable yet.
let delim_pos = tb64.find(TB64_DELIM).ok_or(Tb64Error::MissingDelimiter)?;
let (tag, delim_b64) = tb64.split_at(delim_pos);
if !TaggedBase64::is_safe_base64_tag(tag) {
return Err(Tb64Error::InvalidTag);
}
// Remove the delimiter.
let mut iter = delim_b64.chars();
iter.next();
let value = iter.as_str();
if value.is_empty() {
return Err(Tb64Error::MissingChecksum);
}
// Note: 'printf' debugging is possible like this:
// use web_sys;
// web_sys::console::log_1(&format!("+ {}", &tb64).into());
// Base64 decode the value.
let bytes = TaggedBase64::decode_raw(value)?;
let penultimate = bytes.len() - 1;
let cs = bytes[penultimate];
if cs == TaggedBase64::calc_checksum(tag, &bytes[..penultimate]) {
Ok(TaggedBase64 {
tag: tag.to_string(),
value: bytes[..penultimate].to_vec(),
checksum: cs,
})
} else {
Err(Tb64Error::InvalidChecksum)
}
}
fn calc_checksum(tag: &str, value: &[u8]) -> u8 {
let mut crc8 = CRC::crc8();
crc8.digest(&tag);
crc8.digest(&value);
(crc8.get_crc() as u8) ^ (value.len() as u8)
}
/// Returns true for characters permitted in URL-safe base64 encoding,
/// and false otherwise.
pub fn is_safe_base64_ascii(c: char) -> bool {
('a'..='z').contains(&c)
|| ('A'..='Z').contains(&c)
|| ('0'..='9').contains(&c)
|| (c == '-')
|| (c == '_')
}
/// Checks that an ASCII byte is safe for use in the tag of a
/// TaggedBase64. Because the tags are merely intended to be mnemonic,
/// there's no need to support a large and visually ambiguous
/// character set.
pub fn is_safe_base64_tag(tag: &str) -> bool {
tag.chars().all(TaggedBase64::is_safe_base64_ascii)
}
/// Gets the tag of a TaggedBase64 instance.
pub fn tag(&self) -> String {
self.tag.clone()
}
/// Sets the tag of a TaggedBase64 instance.
pub fn set_tag(&mut self, tag: &str) {
assert!(TaggedBase64::is_safe_base64_tag(tag));
self.tag = tag.to_string();
self.checksum = TaggedBase64::calc_checksum(&self.tag, &self.value);
}
/// Gets the value of a TaggedBase64 instance.
pub fn value(&self) -> Vec<u8> {
self.value.clone()
}
/// Sets the value of a TaggedBase64 instance.
pub fn set_value(&mut self, value: &[u8]) {
self.value = value.to_vec();
self.checksum = TaggedBase64::calc_checksum(&self.tag, &self.value);
}
/// Wraps the underlying base64 encoder.
// WASM doesn't support the most general type.
//
// pub fn encode_raw<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
// base64::encode_config(input, TB64_CONFIG)
// }
pub fn encode_raw(input: &[u8]) -> String {
base64::encode_config(input, TB64_CONFIG)
}
/// Wraps the underlying base64 decoder.
pub fn decode_raw(value: &str) -> Result<Vec<u8>, Tb64Error> {
base64::decode_config(value, TB64_CONFIG).map_err(|err| match err {
base64::DecodeError::InvalidByte(offset, byte) => Tb64Error::InvalidByte(offset, byte),
base64::DecodeError::InvalidLength => Tb64Error::InvalidLength,
base64::DecodeError::InvalidLastSymbol(offset, byte) => {
Tb64Error::InvalidLastSymbol(offset, byte)
}
})
}
}
/// Converts any object that supports the Display trait to a JsValue for
/// passing to Javascript.
///
/// Note: Type parameters aren't supported by `wasm-pack` yet so this
/// can't be included in the TaggedBase64 type implementation.
pub fn to_jsvalue<D: Display>(d: D) -> JsValue {
JsValue::from_str(&format!("{}", d))
}
impl From<Tb64Error> for JsValue {
fn from(error: Tb64Error) -> JsValue {
to_jsvalue(format!("{}", error))
}
}
#[wasm_bindgen]
impl JsTaggedBase64 {
#[wasm_bindgen(constructor)]
pub fn new(tag: &str, value: &[u8]) -> Result<JsTaggedBase64, JsValue> {
let result = TaggedBase64::new(tag, value);
match result {
Ok(tb) => Ok(JsTaggedBase64 { tb64: tb }),
Err(err) => Err(to_jsvalue(err)),
}
}
/// Parses a string of the form tag~value into a TaggedBase64 value.
///
/// The tag is restricted to URL-safe base64 ASCII characters. The tag
/// may be empty. The delimiter is required.
///
/// The value is a base64-encoded string, using the URL-safe character
/// set, and no padding is used.
pub fn parse(tb64: &str) -> Result<TaggedBase64, JsValue> {
let result = TaggedBase64::parse(tb64)?;
Ok(result)
}
/// Gets the tag of a TaggedBase64 instance.
pub fn tag(&self) -> String {
TaggedBase64::tag(&self.tb64)
}
/// Gets the value of a TaggedBase64 instance.
pub fn value(&self) -> Vec<u8> {
TaggedBase64::value(&self.tb64)
}
/// Sets the tag of a JsTaggedBase64 instance.
pub fn set_tag(&mut self, tag: &str) {
self.tb64.set_tag(tag);
}
/// Sets the value of a JsTaggedBase64 instance.
pub fn set_value(&mut self, value: &[u8]) {
self.tb64.set_value(value);
}
}