use flate2::read::DeflateDecoder;
use flate2::write::DeflateEncoder;
use flate2::Compression;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::io::{Read, Write};
use std::sync::LazyLock;
pub const CHARSET: &str = "DXdx0123456789ABCEFGHIJKLMNOPQRSTUVWYZabcefghijklmnopqrstuvwyz-_";
pub const MAGIC: u8 = 0x44;
pub const PREFIX: &str = "dx";
pub const PADDING: char = '=';
const HEADER_SIZE: usize = 3;
const TTL_HEADER_SIZE: usize = 8;
const COMPRESSION_THRESHOLD: usize = 32;
const FLAG_COMPRESSED: u8 = 0x01;
const FLAG_ALGO_DEFLATE: u8 = 0x02;
const FLAG_HAS_TTL: u8 = 0x04;
const VALID_FLAGS_MASK: u8 = FLAG_COMPRESSED | FLAG_ALGO_DEFLATE | FLAG_HAS_TTL;
static CHARSET_BYTES: LazyLock<Vec<u8>> = LazyLock::new(|| CHARSET.as_bytes().to_vec());
static DECODE_MAP: LazyLock<HashMap<u8, u8>> = LazyLock::new(|| {
let mut map = HashMap::new();
for (i, &byte) in CHARSET_BYTES.iter().enumerate() {
map.insert(byte, i as u8);
}
map
});
static CRC16_TABLE: LazyLock<[u16; 256]> = LazyLock::new(|| {
let mut table = [0u16; 256];
for i in 0..256 {
let mut crc = (i as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
table[i] = crc;
}
table
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DxError {
InvalidPrefix,
InvalidLength,
InvalidCharacter(char),
Utf8Error(String),
ChecksumMismatch { expected: u16, actual: u16 },
InvalidHeader,
CompressionError(String),
InvalidFlags(u8),
TtlExpired {
created_at: u64,
ttl_seconds: u32,
expired_at: u64,
},
}
impl fmt::Display for DxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DxError::InvalidPrefix => write!(f, "无效的 DX 编码:缺少 dx 前缀"),
DxError::InvalidLength => write!(f, "无效的 DX 编码:长度不正确"),
DxError::InvalidCharacter(c) => write!(f, "无效的 DX 编码:包含非法字符 '{}'", c),
DxError::Utf8Error(s) => write!(f, "UTF-8 解码错误:{}", s),
DxError::ChecksumMismatch { expected, actual } => {
write!(
f,
"校验和不匹配:期望 0x{:04X},实际 0x{:04X}",
expected, actual
)
}
DxError::InvalidHeader => write!(f, "无效的格式头部"),
DxError::CompressionError(s) => write!(f, "压缩/解压缩错误:{}", s),
DxError::InvalidFlags(flags) => write!(f, "无效的 flags 字节:0x{:02X}", flags),
DxError::TtlExpired {
created_at,
ttl_seconds,
expired_at,
} => {
write!(
f,
"TTL 已过期:创建于 {},有效期 {} 秒,已于 {} 过期",
created_at, ttl_seconds, expired_at
)
}
}
}
}
impl Error for DxError {}
pub type Result<T> = std::result::Result<T, DxError>;
pub fn crc16(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
let index = ((crc >> 8) ^ (byte as u16)) as usize;
crc = (crc << 8) ^ CRC16_TABLE[index];
}
crc
}
fn compress_deflate(data: &[u8]) -> Result<Vec<u8>> {
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(data)
.map_err(|e| DxError::CompressionError(e.to_string()))?;
encoder
.finish()
.map_err(|e| DxError::CompressionError(e.to_string()))
}
fn decompress_deflate(data: &[u8]) -> Result<Vec<u8>> {
let mut decoder = DeflateDecoder::new(data);
let mut decompressed = Vec::new();
decoder
.read_to_end(&mut decompressed)
.map_err(|e| DxError::CompressionError(e.to_string()))?;
Ok(decompressed)
}
fn encode_raw(data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
let mut result = String::with_capacity((data.len() + 2) / 3 * 4);
let charset = &*CHARSET_BYTES;
for chunk in data.chunks(3) {
let b0 = chunk[0];
let b1 = chunk.get(1).copied().unwrap_or(0);
let b2 = chunk.get(2).copied().unwrap_or(0);
let v0 = (b0 >> 2) & 0x3F;
let v1 = ((b0 & 0x03) << 4 | (b1 >> 4)) & 0x3F;
let v2 = ((b1 & 0x0F) << 2 | (b2 >> 6)) & 0x3F;
let v3 = b2 & 0x3F;
result.push(charset[((v0 ^ MAGIC) & 0x3F) as usize] as char);
result.push(charset[((v1 ^ MAGIC) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(charset[((v2 ^ MAGIC) & 0x3F) as usize] as char);
} else {
result.push(PADDING);
}
if chunk.len() > 2 {
result.push(charset[((v3 ^ MAGIC) & 0x3F) as usize] as char);
} else {
result.push(PADDING);
}
}
result
}
fn decode_raw(data: &str) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
if data.len() % 4 != 0 {
return Err(DxError::InvalidLength);
}
let padding_count = if data.ends_with("==") {
2
} else if data.ends_with('=') {
1
} else {
0
};
let output_len = (data.len() / 4) * 3 - padding_count;
let mut result = Vec::with_capacity(output_len);
let decode_map = &*DECODE_MAP;
let data_bytes = data.as_bytes();
for chunk in data_bytes.chunks(4) {
let c0 = chunk[0];
let c1 = chunk[1];
let c2 = chunk[2];
let c3 = chunk[3];
let i0 = *decode_map
.get(&c0)
.ok_or_else(|| DxError::InvalidCharacter(c0 as char))?;
let i1 = *decode_map
.get(&c1)
.ok_or_else(|| DxError::InvalidCharacter(c1 as char))?;
let i2 = if c2 == PADDING as u8 {
0
} else {
*decode_map
.get(&c2)
.ok_or_else(|| DxError::InvalidCharacter(c2 as char))?
};
let i3 = if c3 == PADDING as u8 {
0
} else {
*decode_map
.get(&c3)
.ok_or_else(|| DxError::InvalidCharacter(c3 as char))?
};
let v0 = (i0 ^ MAGIC) & 0x3F;
let v1 = (i1 ^ MAGIC) & 0x3F;
let v2 = (i2 ^ MAGIC) & 0x3F;
let v3 = (i3 ^ MAGIC) & 0x3F;
let b0 = (v0 << 2) | (v1 >> 4);
let b1 = ((v1 & 0x0F) << 4) | (v2 >> 2);
let b2 = ((v2 & 0x03) << 6) | v3;
if result.len() < output_len {
result.push(b0);
}
if result.len() < output_len {
result.push(b1);
}
if result.len() < output_len {
result.push(b2);
}
}
Ok(result)
}
pub fn encode(data: &[u8]) -> String {
encode_with_options(data, true)
}
pub fn encode_with_options(data: &[u8], allow_compression: bool) -> String {
let checksum = crc16(data);
let (flags, payload) = if allow_compression && data.len() >= COMPRESSION_THRESHOLD {
match compress_deflate(data) {
Ok(compressed) => {
if compressed.len() + 2 < data.len() && data.len() <= 65535 {
let mut payload = Vec::with_capacity(2 + compressed.len());
payload.push((data.len() >> 8) as u8);
payload.push((data.len() & 0xFF) as u8);
payload.extend_from_slice(&compressed);
(FLAG_COMPRESSED | FLAG_ALGO_DEFLATE, payload)
} else {
(0u8, data.to_vec())
}
}
Err(_) => {
(0u8, data.to_vec())
}
}
} else {
(0u8, data.to_vec())
};
let header = [flags, (checksum >> 8) as u8, (checksum & 0xFF) as u8];
let mut combined = Vec::with_capacity(HEADER_SIZE + payload.len());
combined.extend_from_slice(&header);
combined.extend_from_slice(&payload);
let mut result = String::with_capacity(PREFIX.len() + (combined.len() + 2) / 3 * 4);
result.push_str(PREFIX);
result.push_str(&encode_raw(&combined));
result
}
pub fn encode_str(s: &str) -> String {
encode(s.as_bytes())
}
pub fn encode_str_with_options(s: &str, allow_compression: bool) -> String {
encode_with_options(s.as_bytes(), allow_compression)
}
pub fn decode(encoded: &str) -> Result<Vec<u8>> {
decode_with_options(encoded, true)
}
pub fn decode_with_options(encoded: &str, check_ttl: bool) -> Result<Vec<u8>> {
if !encoded.starts_with(PREFIX) {
return Err(DxError::InvalidPrefix);
}
let data = &encoded[PREFIX.len()..];
let combined = decode_raw(data)?;
if combined.len() < HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let flags = combined[0];
let expected_checksum = ((combined[1] as u16) << 8) | (combined[2] as u16);
if flags & !VALID_FLAGS_MASK != 0 {
return Err(DxError::InvalidFlags(flags));
}
let payload_start = if flags & FLAG_HAS_TTL != 0 {
if combined.len() < HEADER_SIZE + TTL_HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
if check_ttl {
let created_at = u32::from_be_bytes([
combined[HEADER_SIZE],
combined[HEADER_SIZE + 1],
combined[HEADER_SIZE + 2],
combined[HEADER_SIZE + 3],
]) as u64;
let ttl_seconds = u32::from_be_bytes([
combined[HEADER_SIZE + 4],
combined[HEADER_SIZE + 5],
combined[HEADER_SIZE + 6],
combined[HEADER_SIZE + 7],
]);
if ttl_seconds > 0 {
let expires_at = created_at + ttl_seconds as u64;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now > expires_at {
return Err(DxError::TtlExpired {
created_at,
ttl_seconds,
expired_at: expires_at,
});
}
}
}
HEADER_SIZE + TTL_HEADER_SIZE
} else {
HEADER_SIZE
};
let payload = &combined[payload_start..];
let original_data = if flags & FLAG_COMPRESSED != 0 {
if payload.len() < 2 {
return Err(DxError::InvalidHeader);
}
let _original_size = ((payload[0] as usize) << 8) | (payload[1] as usize);
let compressed_data = &payload[2..];
decompress_deflate(compressed_data)?
} else {
payload.to_vec()
};
let actual_checksum = crc16(&original_data);
if expected_checksum != actual_checksum {
return Err(DxError::ChecksumMismatch {
expected: expected_checksum,
actual: actual_checksum,
});
}
Ok(original_data)
}
pub fn decode_str(encoded: &str) -> Result<String> {
decode_str_with_options(encoded, true)
}
pub fn decode_str_with_options(encoded: &str, check_ttl: bool) -> Result<String> {
let bytes = decode_with_options(encoded, check_ttl)?;
String::from_utf8(bytes).map_err(|e| DxError::Utf8Error(e.to_string()))
}
pub fn is_encoded(s: &str) -> bool {
if !s.starts_with(PREFIX) {
return false;
}
let data = &s[PREFIX.len()..];
if data.is_empty() || data.len() % 4 != 0 {
return false;
}
let decode_map = &*DECODE_MAP;
for (i, c) in data.bytes().enumerate() {
if c == PADDING as u8 {
if i < data.len() - 2 {
return false;
}
} else if !decode_map.contains_key(&c) {
return false;
}
}
true
}
pub fn verify(encoded: &str) -> Result<bool> {
match decode(encoded) {
Ok(_) => Ok(true),
Err(DxError::ChecksumMismatch { .. }) => Ok(false),
Err(e) => Err(e),
}
}
pub fn get_checksum(encoded: &str) -> Result<(u16, u16)> {
if !encoded.starts_with(PREFIX) {
return Err(DxError::InvalidPrefix);
}
let data = &encoded[PREFIX.len()..];
let combined = decode_raw(data)?;
if combined.len() < HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let flags = combined[0];
let stored = ((combined[1] as u16) << 8) | (combined[2] as u16);
let payload = &combined[HEADER_SIZE..];
let original_data = if flags & FLAG_COMPRESSED != 0 {
if payload.len() < 2 {
return Err(DxError::InvalidHeader);
}
let compressed_data = &payload[2..];
decompress_deflate(compressed_data)?
} else {
payload.to_vec()
};
let computed = crc16(&original_data);
Ok((stored, computed))
}
pub fn is_compressed(encoded: &str) -> Result<bool> {
if !encoded.starts_with(PREFIX) {
return Err(DxError::InvalidPrefix);
}
let data = &encoded[PREFIX.len()..];
let combined = decode_raw(data)?;
if combined.len() < HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let flags = combined[0];
Ok(flags & FLAG_COMPRESSED != 0)
}
#[derive(Debug, Clone)]
pub struct TtlInfo {
pub created_at: u64,
pub ttl_seconds: u32,
pub expires_at: Option<u64>,
pub is_expired: bool,
}
pub fn has_ttl(encoded: &str) -> Result<bool> {
if !encoded.starts_with(PREFIX) {
return Err(DxError::InvalidPrefix);
}
let data = &encoded[PREFIX.len()..];
let combined = decode_raw(data)?;
if combined.len() < HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let flags = combined[0];
Ok(flags & FLAG_HAS_TTL != 0)
}
pub fn get_ttl_info(encoded: &str) -> Result<Option<TtlInfo>> {
if !encoded.starts_with(PREFIX) {
return Err(DxError::InvalidPrefix);
}
let data = &encoded[PREFIX.len()..];
let combined = decode_raw(data)?;
if combined.len() < HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let flags = combined[0];
if flags & FLAG_HAS_TTL == 0 {
return Ok(None);
}
if combined.len() < HEADER_SIZE + TTL_HEADER_SIZE {
return Err(DxError::InvalidHeader);
}
let created_at = u32::from_be_bytes([
combined[HEADER_SIZE],
combined[HEADER_SIZE + 1],
combined[HEADER_SIZE + 2],
combined[HEADER_SIZE + 3],
]) as u64;
let ttl_seconds = u32::from_be_bytes([
combined[HEADER_SIZE + 4],
combined[HEADER_SIZE + 5],
combined[HEADER_SIZE + 6],
combined[HEADER_SIZE + 7],
]);
let (expires_at, is_expired) = if ttl_seconds == 0 {
(None, false)
} else {
let expires = created_at + ttl_seconds as u64;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
(Some(expires), now > expires)
};
Ok(Some(TtlInfo {
created_at,
ttl_seconds,
expires_at,
is_expired,
}))
}
pub fn is_expired(encoded: &str) -> Result<bool> {
match get_ttl_info(encoded)? {
Some(info) => Ok(info.is_expired),
None => Ok(false), }
}
pub fn encode_with_ttl(data: &[u8], ttl_seconds: u32) -> String {
encode_with_ttl_and_options(data, ttl_seconds, true)
}
pub fn encode_with_ttl_and_options(data: &[u8], ttl_seconds: u32, allow_compression: bool) -> String {
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0);
let checksum = crc16(data);
let (mut flags, payload) = if allow_compression && data.len() >= COMPRESSION_THRESHOLD {
match compress_deflate(data) {
Ok(compressed) => {
if compressed.len() + 2 < data.len() && data.len() <= 65535 {
let mut payload = Vec::with_capacity(2 + compressed.len());
payload.push((data.len() >> 8) as u8);
payload.push((data.len() & 0xFF) as u8);
payload.extend_from_slice(&compressed);
(FLAG_COMPRESSED | FLAG_ALGO_DEFLATE, payload)
} else {
(0u8, data.to_vec())
}
}
Err(_) => (0u8, data.to_vec()),
}
} else {
(0u8, data.to_vec())
};
flags |= FLAG_HAS_TTL;
let mut combined = Vec::with_capacity(HEADER_SIZE + TTL_HEADER_SIZE + payload.len());
combined.push(flags);
combined.push((checksum >> 8) as u8);
combined.push((checksum & 0xFF) as u8);
combined.extend_from_slice(&created_at.to_be_bytes());
combined.extend_from_slice(&ttl_seconds.to_be_bytes());
combined.extend_from_slice(&payload);
let mut result = String::with_capacity(PREFIX.len() + (combined.len() + 2) / 3 * 4);
result.push_str(PREFIX);
result.push_str(&encode_raw(&combined));
result
}
pub fn encode_str_with_ttl(s: &str, ttl_seconds: u32) -> String {
encode_with_ttl(s.as_bytes(), ttl_seconds)
}
#[derive(Debug, Clone)]
pub struct Info {
pub name: &'static str,
pub version: &'static str,
pub author: &'static str,
pub charset: &'static str,
pub prefix: &'static str,
pub magic: u8,
pub padding: char,
pub checksum: &'static str,
pub compression: &'static str,
pub compression_threshold: usize,
}
pub fn get_info() -> Info {
Info {
name: "DX Encoding",
version: "2.3.0",
author: "Dogxi",
charset: CHARSET,
prefix: PREFIX,
magic: MAGIC,
padding: PADDING,
checksum: "CRC16-CCITT",
compression: "DEFLATE",
compression_threshold: COMPRESSION_THRESHOLD,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_string() {
let original = "Hello";
let encoded = encode_str(original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
assert!(encoded.starts_with("dx"));
}
#[test]
fn test_chinese_string() {
let original = "你好,世界!";
let encoded = encode_str(original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_emoji() {
let original = "🎉🚀✨";
let encoded = encode_str(original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_empty_string() {
let original = "";
let encoded = encode_str(original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
assert!(encoded.starts_with("dx"));
}
#[test]
fn test_binary_data() {
let original: Vec<u8> = vec![0x00, 0x01, 0x02, 0xFE, 0xFF];
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_all_byte_values() {
let original: Vec<u8> = (0..=255).collect();
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_is_encoded() {
let encoded = encode_str("Hello");
assert!(is_encoded(&encoded));
assert!(!is_encoded("hello"));
}
#[test]
fn test_decode_invalid_prefix() {
let result = decode("invalid");
assert!(matches!(result, Err(DxError::InvalidPrefix)));
}
#[test]
fn test_decode_invalid_length() {
let result = decode("dxABC");
assert!(matches!(result, Err(DxError::InvalidLength)));
}
#[test]
fn test_checksum_verification() {
let encoded = encode_str("Hello");
assert!(verify(&encoded).unwrap());
let (stored, computed) = get_checksum(&encoded).unwrap();
assert_eq!(stored, computed);
}
#[test]
fn test_checksum_mismatch() {
let encoded = encode_str("Hello World Test Data");
let mut chars: Vec<char> = encoded.chars().collect();
if chars.len() > 10 {
let pos = 10;
let original_char = chars[pos];
chars[pos] = if original_char == 'A' { 'B' } else { 'A' };
}
let modified: String = chars.into_iter().collect();
let result = decode(&modified);
assert!(
matches!(result, Err(DxError::ChecksumMismatch { .. }))
|| matches!(result, Err(DxError::InvalidCharacter(_)))
);
}
#[test]
fn test_get_info() {
let info = get_info();
assert_eq!(info.name, "DX Encoding");
assert_eq!(info.author, "Dogxi");
assert_eq!(info.prefix, "dx");
assert_eq!(info.magic, 0x44);
assert_eq!(info.charset.len(), 64);
assert_eq!(info.version, "2.3.0");
assert_eq!(info.checksum, "CRC16-CCITT");
assert_eq!(info.compression, "DEFLATE");
}
#[test]
fn test_various_lengths() {
for length in 0..100 {
let original: Vec<u8> = (0..length).map(|i| (i % 256) as u8).collect();
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original, "长度 {} 失败", length);
}
}
#[test]
fn test_crc16() {
assert_eq!(crc16(&[]), 0xFFFF);
let data = b"123456789";
let crc = crc16(data);
assert_eq!(crc, 0x29B1);
}
#[test]
fn test_crc16_deterministic() {
let data = b"Hello, World!";
let crc1 = crc16(data);
let crc2 = crc16(data);
assert_eq!(crc1, crc2);
}
#[test]
fn test_verify_function() {
let encoded = encode_str("Test data for verification");
assert!(verify(&encoded).unwrap());
}
#[test]
fn test_short_data_not_compressed() {
let original = "Short";
let encoded = encode_str(original);
assert!(!is_compressed(&encoded).unwrap());
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_long_repetitive_data_compressed() {
let original = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let encoded = encode_str(original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
assert!(is_compressed(&encoded).unwrap());
}
#[test]
fn test_compression_saves_space() {
let original = "Hello World! ".repeat(100);
let encoded_compressed = encode_str(&original);
let encoded_uncompressed = encode_str_with_options(&original, false);
assert!(
encoded_compressed.len() < encoded_uncompressed.len(),
"压缩版本 ({}) 应该比未压缩版本 ({}) 短",
encoded_compressed.len(),
encoded_uncompressed.len()
);
assert_eq!(decode_str(&encoded_compressed).unwrap(), original);
assert_eq!(decode_str(&encoded_uncompressed).unwrap(), original);
}
#[test]
fn test_incompressible_data() {
let original: Vec<u8> = (0..100).map(|i| (i * 7 + 13) as u8).collect();
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_encode_without_compression() {
let original = "A".repeat(100);
let encoded = encode_str_with_options(&original, false);
assert!(!is_compressed(&encoded).unwrap());
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_compression_threshold() {
let short_data = "x".repeat(COMPRESSION_THRESHOLD - 1);
let encoded_short = encode_str(&short_data);
assert!(!is_compressed(&encoded_short).unwrap());
let long_data = "x".repeat(COMPRESSION_THRESHOLD + 10);
let encoded_long = encode_str(&long_data);
assert!(is_compressed(&encoded_long).unwrap());
}
#[test]
fn test_large_data_compression() {
let original = "The quick brown fox jumps over the lazy dog. ".repeat(500);
let encoded = encode_str(&original);
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
assert!(verify(&encoded).unwrap());
}
#[test]
fn test_encode_with_ttl() {
let original = b"Secret Data";
let encoded = encode_with_ttl(original, 3600);
assert!(encoded.starts_with("dx"));
assert!(has_ttl(&encoded).unwrap());
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_encode_str_with_ttl() {
let original = "临时令牌";
let encoded = encode_str_with_ttl(original, 1800);
assert!(has_ttl(&encoded).unwrap());
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_ttl_info() {
let encoded = encode_with_ttl(b"Test", 3600);
let info = get_ttl_info(&encoded).unwrap().unwrap();
assert_eq!(info.ttl_seconds, 3600);
assert!(!info.is_expired);
assert!(info.expires_at.is_some());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(info.created_at <= now && info.created_at >= now - 5);
}
#[test]
fn test_ttl_zero_never_expires() {
let encoded = encode_with_ttl(b"Forever", 0);
let info = get_ttl_info(&encoded).unwrap().unwrap();
assert_eq!(info.ttl_seconds, 0);
assert!(info.expires_at.is_none());
assert!(!info.is_expired);
assert!(!is_expired(&encoded).unwrap());
}
#[test]
fn test_no_ttl_returns_none() {
let encoded = encode(b"No TTL");
assert!(!has_ttl(&encoded).unwrap());
assert!(get_ttl_info(&encoded).unwrap().is_none());
assert!(!is_expired(&encoded).unwrap());
}
#[test]
fn test_ttl_with_compression() {
let original = "Repeated data for compression test. ".repeat(50);
let encoded = encode_str_with_ttl(&original, 7200);
assert!(has_ttl(&encoded).unwrap());
assert!(is_compressed(&encoded).unwrap());
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_ttl_without_compression() {
let original = "Short";
let encoded = encode_with_ttl_and_options(original.as_bytes(), 3600, false);
assert!(has_ttl(&encoded).unwrap());
assert!(!is_compressed(&encoded).unwrap());
let decoded = decode_str(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_decode_skip_ttl_check() {
let encoded = encode_with_ttl(b"Data", 1);
let decoded = decode_with_options(&encoded, false).unwrap();
assert_eq!(decoded, b"Data");
}
#[test]
fn test_is_expired_function() {
let encoded = encode_with_ttl(b"Data", 86400); assert!(!is_expired(&encoded).unwrap());
}
}