trailbase_sqlvalue/
lib.rs1#![forbid(unsafe_code, clippy::unwrap_used)]
2#![allow(clippy::needless_return)]
3#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
4
5use base64::prelude::*;
6use serde::{Deserialize, Serialize};
7use ts_rs::TS;
8
9#[derive(Debug, Clone, thiserror::Error)]
10pub enum DecodeError {
11 #[error("Base64: {0}")]
12 Base64(base64::DecodeError),
13 #[error("Hex")]
14 Hex,
15}
16
17#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, TS)]
18#[ts(export)]
19pub enum Blob {
20 Array(Vec<u8>),
21 Base64UrlSafe(String),
23 Hex(String),
24}
25
26impl Blob {
27 pub fn to_b64_url_safe(&self) -> Result<String, DecodeError> {
28 return Ok(match self {
29 Blob::Array(v) => BASE64_URL_SAFE.encode(v),
30 Blob::Base64UrlSafe(s) => s.clone(),
31 Blob::Hex(s) => BASE64_URL_SAFE.encode(decode_hex(s)?),
32 });
33 }
34
35 pub fn into_b64_url_safe(self) -> Result<String, DecodeError> {
36 return Ok(match self {
37 Blob::Array(v) => BASE64_URL_SAFE.encode(&v),
38 Blob::Base64UrlSafe(s) => s,
39 Blob::Hex(s) => BASE64_URL_SAFE.encode(decode_hex(&s)?),
40 });
41 }
42
43 pub fn to_bytes(&self) -> Result<Vec<u8>, DecodeError> {
44 return Ok(match self {
45 Blob::Array(v) => v.clone(),
46 Blob::Base64UrlSafe(s) => BASE64_URL_SAFE.decode(s).map_err(DecodeError::Base64)?,
47 Blob::Hex(s) => decode_hex(s)?,
48 });
49 }
50
51 pub fn into_bytes(self) -> Result<Vec<u8>, DecodeError> {
52 return Ok(match self {
53 Blob::Array(v) => v,
54 Blob::Base64UrlSafe(s) => BASE64_URL_SAFE.decode(&s).map_err(DecodeError::Base64)?,
55 Blob::Hex(s) => decode_hex(&s)?,
56 });
57 }
58}
59
60#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, TS)]
62#[ts(export)]
63pub enum SqlValue {
64 Null,
65 Integer(i64),
66 Real(f64),
67 Text(String),
68 Blob(Blob),
69}
70
71impl Default for SqlValue {
72 fn default() -> Self {
73 return SqlValue::Null;
74 }
75}
76
77#[cfg(feature = "rusqlite")]
78impl TryFrom<SqlValue> for trailbase_sqlite::Value {
79 type Error = DecodeError;
80
81 fn try_from(value: SqlValue) -> Result<Self, Self::Error> {
82 use trailbase_sqlite::Value;
83
84 return Ok(match value {
85 SqlValue::Null => Value::Null,
86 SqlValue::Integer(v) => Value::Integer(v),
87 SqlValue::Real(v) => Value::Real(v),
88 SqlValue::Text(v) => Value::Text(v),
89 SqlValue::Blob(b) => match b {
90 Blob::Array(v) => Value::Blob(v),
91 Blob::Base64UrlSafe(v) => {
92 Value::Blob(BASE64_URL_SAFE.decode(v).map_err(DecodeError::Base64)?)
93 }
94 Blob::Hex(v) => Value::Blob(decode_hex(&v)?),
95 },
96 });
97 }
98}
99
100#[cfg(feature = "rusqlite")]
101impl From<trailbase_sqlite::Value> for SqlValue {
102 fn from(value: trailbase_sqlite::Value) -> Self {
103 use trailbase_sqlite::Value;
104
105 return match value {
106 Value::Null => SqlValue::Null,
107 Value::Integer(v) => SqlValue::Integer(v),
108 Value::Real(v) => SqlValue::Real(v),
109 Value::Text(v) => SqlValue::Text(v),
110 Value::Blob(v) => SqlValue::Blob(Blob::Base64UrlSafe(BASE64_URL_SAFE.encode(v))),
111 };
112 }
113}
114
115#[cfg(feature = "rusqlite")]
116impl From<&trailbase_sqlite::Value> for SqlValue {
117 fn from(value: &trailbase_sqlite::Value) -> Self {
118 use trailbase_sqlite::Value;
119
120 return match value {
121 Value::Null => SqlValue::Null,
122 Value::Integer(v) => SqlValue::Integer(*v),
123 Value::Real(v) => SqlValue::Real(*v),
124 Value::Text(v) => SqlValue::Text(v.clone()),
125 Value::Blob(v) => SqlValue::Blob(Blob::Base64UrlSafe(BASE64_URL_SAFE.encode(v))),
126 };
127 }
128}
129
130fn decode_hex(s: &str) -> Result<Vec<u8>, DecodeError> {
132 if !s.len().is_multiple_of(2) {
133 return Err(DecodeError::Hex);
134 }
135
136 return (0..s.len())
137 .step_by(2)
138 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| DecodeError::Hex))
139 .collect();
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn decode_hex_test() {
148 assert_eq!(Vec::<u8>::new(), decode_hex("").unwrap());
149 assert!(decode_hex("BA5E").is_ok());
150 assert!(decode_hex("ba5e").is_ok());
151 assert!(decode_hex("ba5").is_err());
152 }
153}