Skip to content

Commit

Permalink
fu
Browse files Browse the repository at this point in the history
  • Loading branch information
matze committed Mar 2, 2025
1 parent d6175fd commit 0de9896
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 45 deletions.
4 changes: 2 additions & 2 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ mod tests {
fn cache_key() {
let key = Key::from_str("bJZCna").unwrap();
assert_eq!(key.id(), "bJZCna");
assert_eq!(key.id, 104651828.into());
assert_eq!(key.id, 104651828u32.into());
assert_eq!(key.ext, "txt");

let key = Key::from_str("sIiFec.rs").unwrap();
assert_eq!(key.id(), "sIiFec");
assert_eq!(key.id, 1243750162.into());
assert_eq!(key.id, 1243750162u32.into());
assert_eq!(key.ext, "rs");

assert!(Key::from_str("foo").is_err());
Expand Down
8 changes: 4 additions & 4 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,15 +458,15 @@ mod tests {
..Default::default()
};

let id = Id::from(1234);
let id = Id::from(1234u32);
db.insert(id, entry).await?;

let entry = db.get(id, None).await?.unwrap_inner();
assert_eq!(entry.text, "hello world");
assert!(entry.uid.is_some());
assert_eq!(entry.uid.unwrap(), 10);

let result = db.get(Id::from(5678), None).await;
let result = db.get(Id::from(5678u32), None).await;
assert!(result.is_err());

Ok(())
Expand All @@ -481,7 +481,7 @@ mod tests {
..Default::default()
};

let id = Id::from(1234);
let id = Id::from(1234u32);
db.insert(id, entry).await?;

tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
Expand All @@ -496,7 +496,7 @@ mod tests {
async fn delete() -> Result<(), Box<dyn std::error::Error>> {
let db = new_db()?;

let id = Id::from(1234);
let id = Id::from(1234u32);
db.insert(id, write::Entry::default()).await?;

assert!(db.get(id, None).await.is_ok());
Expand Down
143 changes: 104 additions & 39 deletions src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ const CHAR_TABLE: &[char; 64] = &[
'5', '6', '7', '8', '9', '-', '+',
];

/// Represents a 32-bit integer either numerically or mapped to a 6 character string.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Id {
n: u32,
pub(crate) enum Id {
/// Six-character pre 3.0 identifiers.
Id32(u32),
/// Eleven-character identifiers.
Id64(i64),
}

impl Id {
/// Generate a new random [`Id`]. According to the [`rand::rng()`] documentation this should be
/// fast and not require additional an `spawn_blocking()` call.
pub fn new() -> Self {
let n = rand::rng().random::<u32>();
Self { n }
Self::Id64(rand::rng().random::<i64>())
}

/// Return i64 representation for database storage purposes.
pub fn to_i64(self) -> i64 {
self.n.into()
match self {
Self::Id32(n) => n.into(),
Self::Id64(n) => n,
}
}

/// Generate a URL path from the string representation and `entry`'s extension.
Expand All @@ -41,47 +45,89 @@ impl Id {

impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::with_capacity(6);

s.push(CHAR_TABLE[((self.n >> 26) & 0x3f) as usize]);
s.push(CHAR_TABLE[((self.n >> 20) & 0x3f) as usize]);
s.push(CHAR_TABLE[((self.n >> 14) & 0x3f) as usize]);
s.push(CHAR_TABLE[((self.n >> 8) & 0x3f) as usize]);
s.push(CHAR_TABLE[((self.n >> 2) & 0x3f) as usize]);
s.push(CHAR_TABLE[(self.n & 0x3) as usize]);

write!(f, "{s}")
match self {
Self::Id32(n) => {
let mut s = String::with_capacity(6);

s.push(CHAR_TABLE[((n >> 26) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 20) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 14) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 8) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 2) & 0x3f) as usize]);
s.push(CHAR_TABLE[(n & 0x3) as usize]);

write!(f, "{s}")
}
Self::Id64(n) => {
let mut s = String::with_capacity(11);

s.push(CHAR_TABLE[((n >> 58) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 52) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 46) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 40) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 34) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 28) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 22) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 16) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 10) & 0x3f) as usize]);
s.push(CHAR_TABLE[((n >> 4) & 0x3f) as usize]);
s.push(CHAR_TABLE[(n & 0x3) as usize]);

write!(f, "{s}")
}
}
}
}

impl FromStr for Id {
type Err = Error;

fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() != 6 {
return Err(Error::WrongSize);
}

let mut n: u32 = 0;

for (pos, char) in value.chars().enumerate() {
let bits: Option<u32> = CHAR_TABLE.iter().enumerate().find_map(|(bits, c)| {
(char == *c).then(|| bits.try_into().expect("bits not 32 bits"))
});
if value.len() == 6 {
let mut n: u32 = 0;

for (pos, char) in value.chars().enumerate() {
let bits: Option<u32> = CHAR_TABLE.iter().enumerate().find_map(|(bits, c)| {
(char == *c).then(|| bits.try_into().expect("bits not 32 bits"))
});

match bits {
None => return Err(Error::IllegalCharacters),
Some(bits) => {
if pos < 5 {
n = (n << 6) | bits;
} else {
n = (n << 2) | bits;
}
}
}
}

match bits {
None => return Err(Error::IllegalCharacters),
Some(bits) => {
if pos < 5 {
n = (n << 6) | bits;
} else {
n = (n << 2) | bits;
Ok(Self::Id32(n))
} else if value.len() == 11 {
let mut n: i64 = 0;

for (pos, char) in value.chars().enumerate() {
let bits: Option<i64> = CHAR_TABLE.iter().enumerate().find_map(|(bits, c)| {
(char == *c).then(|| bits.try_into().expect("bits not 64 bits"))
});

match bits {
None => return Err(Error::IllegalCharacters),
Some(bits) => {
if pos < 10 {
n = (n << 6) | bits;
} else {
n = (n << 2) | bits;
}
}
}
}
}

Ok(Self { n })
Ok(Self::Id64(n))
} else {
return Err(Error::WrongSize);
}
}
}

Expand All @@ -91,31 +137,50 @@ mod tests {

impl From<u32> for Id {
fn from(n: u32) -> Self {
Self { n }
Self::Id32(n)
}
}

impl From<i64> for Id {
fn from(n: i64) -> Self {
Self::Id64(n)
}
}

#[test]
fn convert_i64_to_id_and_back() {
let id = Id::from(0);
let id = Id::from(0u32);
assert_eq!(id.to_string(), "aaaaaa");
assert_eq!(id.to_i64(), 0);

let id = Id::from(0xffffffff);
let id = Id::from(0i64);
assert_eq!(id.to_string(), "aaaaaaaaaaa");
assert_eq!(id.to_i64(), 0);

let id = Id::from(0xffffffffu32);
assert_eq!(id.to_string(), "+++++d");
assert_eq!(id.to_i64(), 0xffffffff);

let id = Id::from(0xfffffffffffffffi64);
assert_eq!(id.to_string(), "d+++++++++d");
assert_eq!(id.to_i64(), 0xfffffffffffffff);
}

#[test]
fn convert_string_to_id_and_back() {
let id = Id::from_str("bJZCna").unwrap();
assert_eq!(id, Id { n: 104651828 });
assert_eq!(id.to_i64(), 104651828);
assert_eq!(id.to_string(), "bJZCna");

let id = Id::from_str("bJZCnaP+152").unwrap();
assert_eq!(id.to_i64(), 112369044725691894);
assert_eq!(id.to_string(), "bJZCnaP+152");
}

#[test]
fn conversion_failures() {
assert!(Id::from_str("abDE+-").is_ok());
assert!(Id::from_str("abDE+-12345").is_ok());
assert!(matches!(
Id::from_str("#bDE+-"),
Err(Error::IllegalCharacters)
Expand Down

0 comments on commit 0de9896

Please sign in to comment.