chuchi_postgres_types/
uid.rs

1use std::fmt;
2use std::str::FromStr;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine};
6use base64::DecodeError;
7use rand::{rngs::OsRng, RngCore};
8
9/// A UniqueId that can be used within a database.
10/// Is not cryptographically secure and could be bruteforced.
11///
12/// Contains 10bytes
13/// - 0..5 are seconds since the UNIX_EPOCH
14/// - 5..10 are random
15#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16// graphql
17#[cfg_attr(feature = "juniper", derive(juniper::GraphQLScalar))]
18#[cfg_attr(feature = "juniper", graphql(with = graphql))]
19pub struct UniqueId([u8; 10]);
20
21impl UniqueId {
22	pub fn new() -> Self {
23		let secs_bytes = SystemTime::now()
24			.duration_since(UNIX_EPOCH)
25			.expect("SystemTime before UNIX EPOCH!")
26			.as_secs()
27			.to_be_bytes();
28
29		let mut bytes = [0u8; 10];
30		bytes[..5].copy_from_slice(&secs_bytes[3..8]);
31
32		OsRng.fill_bytes(&mut bytes[5..]);
33
34		Self(bytes)
35	}
36
37	/// This creates a unique id with it's raw content
38	/// making it able to be called in a const context.
39	pub const fn from_raw(inner: [u8; 10]) -> Self {
40		Self(inner)
41	}
42
43	pub fn from_slice_unchecked(slice: &[u8]) -> Self {
44		let mut bytes = [0u8; 10];
45		bytes.copy_from_slice(slice);
46		Self(bytes)
47	}
48
49	pub fn to_b64(&self) -> String {
50		URL_SAFE_NO_PAD.encode(self.0)
51	}
52
53	/// If the string is not 14 bytes long returns InvalidLength
54	pub fn parse_from_b64<T>(b64: T) -> Result<Self, DecodeError>
55	where
56		T: AsRef<[u8]>,
57	{
58		if b64.as_ref().len() != 14 {
59			return Err(DecodeError::InvalidLength(b64.as_ref().len()));
60		}
61
62		let mut bytes = [0u8; 10];
63		URL_SAFE_NO_PAD
64			.decode_slice_unchecked(b64, &mut bytes)
65			.map(|n| assert_eq!(n, bytes.len()))
66			.map(|_| Self(bytes))
67	}
68
69	pub fn from_bytes(bytes: [u8; 10]) -> Self {
70		Self(bytes)
71	}
72
73	pub fn into_bytes(self) -> [u8; 10] {
74		self.0
75	}
76
77	pub fn since_unix_secs(&self) -> u64 {
78		let mut bytes = [0u8; 8];
79		bytes[3..].copy_from_slice(&self.0[..5]);
80		u64::from_be_bytes(bytes)
81	}
82
83	pub fn as_slice(&self) -> &[u8] {
84		&self.0
85	}
86}
87
88impl fmt::Debug for UniqueId {
89	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90		f.debug_tuple("UniqueId").field(&self.to_b64()).finish()
91	}
92}
93
94impl fmt::Display for UniqueId {
95	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96		self.to_b64().fmt(f)
97	}
98}
99
100impl FromStr for UniqueId {
101	type Err = DecodeError;
102
103	fn from_str(s: &str) -> Result<Self, Self::Err> {
104		Self::parse_from_b64(s)
105	}
106}
107
108// SERDE
109
110#[cfg(feature = "serde")]
111mod impl_serde {
112	use super::*;
113
114	use std::borrow::Cow;
115
116	use serde::de::{Deserializer, Error};
117	use serde::ser::Serializer;
118	use serde::{Deserialize, Serialize};
119
120	impl Serialize for UniqueId {
121		fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122		where
123			S: Serializer,
124		{
125			serializer.serialize_str(&self.to_b64())
126		}
127	}
128
129	impl<'de> Deserialize<'de> for UniqueId {
130		fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131		where
132			D: Deserializer<'de>,
133		{
134			let s: Cow<'_, str> = Deserialize::deserialize(deserializer)?;
135			let s = s.as_ref();
136			if s.len() == 14 {
137				UniqueId::parse_from_b64(s).map_err(D::Error::custom)
138			} else {
139				Err(D::Error::custom(
140					"expected string with exactly 14 characters",
141				))
142			}
143		}
144	}
145}
146
147#[cfg(feature = "postgres")]
148mod postgres {
149	use bytes::BytesMut;
150	use postgres_types::{to_sql_checked, FromSql, IsNull, ToSql, Type};
151
152	use super::*;
153
154	impl ToSql for UniqueId {
155		fn to_sql(
156			&self,
157			ty: &Type,
158			out: &mut BytesMut,
159		) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>>
160		where
161			Self: Sized,
162		{
163			self.to_b64().to_sql(ty, out)
164		}
165
166		fn accepts(ty: &Type) -> bool
167		where
168			Self: Sized,
169		{
170			<&str as ToSql>::accepts(ty)
171		}
172
173		to_sql_checked!();
174	}
175
176	impl<'r> FromSql<'r> for UniqueId {
177		fn from_sql(
178			ty: &Type,
179			raw: &'r [u8],
180		) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
181			let s = <&str as FromSql>::from_sql(ty, raw)?;
182			UniqueId::parse_from_b64(s).map_err(Into::into)
183		}
184
185		fn accepts(ty: &Type) -> bool {
186			<&str as FromSql>::accepts(ty)
187		}
188	}
189}
190
191#[cfg(feature = "protobuf")]
192mod protobuf {
193	use super::*;
194
195	use protopuffer::{
196		bytes::BytesWrite,
197		decode::{DecodeError, DecodeMessage, FieldKind},
198		encode::{
199			EncodeError, EncodeMessage, FieldOpt, MessageEncoder, SizeBuilder,
200		},
201		WireType,
202	};
203
204	impl EncodeMessage for UniqueId {
205		const WIRE_TYPE: WireType = WireType::Len;
206
207		fn is_default(&self) -> bool {
208			false
209		}
210
211		fn encoded_size(
212			&mut self,
213			field: Option<FieldOpt>,
214			builder: &mut SizeBuilder,
215		) -> Result<(), EncodeError> {
216			self.0.encoded_size(field, builder)
217		}
218
219		fn encode<B>(
220			&mut self,
221			field: Option<FieldOpt>,
222			encoder: &mut MessageEncoder<B>,
223		) -> Result<(), EncodeError>
224		where
225			B: BytesWrite,
226		{
227			self.0.encode(field, encoder)
228		}
229	}
230
231	impl<'m> DecodeMessage<'m> for UniqueId {
232		const WIRE_TYPE: WireType = WireType::Len;
233
234		fn decode_default() -> Self {
235			Self::from_raw([0; 10])
236		}
237
238		fn merge(
239			&mut self,
240			kind: FieldKind<'m>,
241			is_field: bool,
242		) -> Result<(), DecodeError> {
243			self.0.merge(kind, is_field)
244		}
245	}
246}
247
248#[cfg(feature = "juniper")]
249mod graphql {
250	use super::*;
251
252	use juniper::{
253		InputValue, ParseScalarResult, ScalarToken, ScalarValue, Value,
254	};
255
256	pub(crate) fn to_output<S: ScalarValue>(v: &UniqueId) -> Value<S> {
257		Value::scalar(v.to_string())
258	}
259
260	pub(crate) fn from_input<S: ScalarValue>(
261		v: &InputValue<S>,
262	) -> Result<UniqueId, String> {
263		v.as_string_value()
264			.ok_or("Expected a string")?
265			.parse()
266			.map_err(|e: DecodeError| e.to_string())
267	}
268
269	pub(crate) fn parse_token<S: ScalarValue>(
270		value: ScalarToken<'_>,
271	) -> ParseScalarResult<S> {
272		<String as juniper::ParseScalarValue<S>>::from_str(value)
273	}
274}
275
276#[cfg(all(test, feature = "serde"))]
277mod tests {
278
279	use super::*;
280	use serde_json::{from_str, from_value, Value};
281
282	// abcdefghijklmnopqrstuvwxyz
283
284	#[test]
285	fn serde_test() {
286		let s = "\"AGCGeWIDTlipbg\"";
287		let d: UniqueId = from_str(s).unwrap();
288		assert_eq!(d.to_string(), "AGCGeWIDTlipbg");
289
290		let v = Value::String("AGCGeWIDTlipbg".into());
291		let d: UniqueId = from_value(v).unwrap();
292		assert_eq!(d.to_string(), "AGCGeWIDTlipbg");
293	}
294}