Skip to main content

alien_bindings/providers/kv/
mod.rs

1#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
2use crate::error::{ErrorData, Result};
3#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
4use alien_error::{AlienError, Context, IntoAlienError};
5#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
6use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
7#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
8use chrono::Utc;
9#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
10use serde::{Deserialize, Serialize};
11
12#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
13const VERSION_TOKEN_FORMAT: u8 = 1;
14
15#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
16#[derive(Debug, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18struct VersionToken {
19    format: u8,
20    key: String,
21    backend_version: String,
22    expires_at_millis: Option<i64>,
23}
24
25#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
26pub(crate) struct DecodedVersion {
27    pub backend_version: String,
28    pub expired: bool,
29}
30
31#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
32pub(crate) fn encode_version(
33    key: &str,
34    backend_version: String,
35    expires_at_millis: Option<i64>,
36) -> Result<String> {
37    let bytes = serde_json::to_vec(&VersionToken {
38        format: VERSION_TOKEN_FORMAT,
39        key: key.to_string(),
40        backend_version,
41        expires_at_millis,
42    })
43    .into_alien_error()
44    .context(ErrorData::InvalidInput {
45        operation_context: "KV version encoding".to_string(),
46        details: "Failed to serialize the version token".to_string(),
47        field_name: Some("version".to_string()),
48    })?;
49    Ok(URL_SAFE_NO_PAD.encode(bytes))
50}
51
52#[cfg(any(feature = "aws", feature = "azure", feature = "gcp", feature = "local"))]
53pub(crate) fn decode_version(key: &str, encoded: &str) -> Result<DecodedVersion> {
54    let bytes =
55        URL_SAFE_NO_PAD
56            .decode(encoded)
57            .into_alien_error()
58            .context(ErrorData::InvalidInput {
59                operation_context: "KV version decoding".to_string(),
60                details: "Invalid version token encoding".to_string(),
61                field_name: Some("ifVersion".to_string()),
62            })?;
63    let token: VersionToken =
64        serde_json::from_slice(&bytes)
65            .into_alien_error()
66            .context(ErrorData::InvalidInput {
67                operation_context: "KV version decoding".to_string(),
68                details: "Invalid version token data".to_string(),
69                field_name: Some("ifVersion".to_string()),
70            })?;
71    if token.format != VERSION_TOKEN_FORMAT || token.key != key {
72        return Err(AlienError::new(ErrorData::InvalidInput {
73            operation_context: "KV version validation".to_string(),
74            details: "Version token does not belong to this key".to_string(),
75            field_name: Some("ifVersion".to_string()),
76        }));
77    }
78
79    Ok(DecodedVersion {
80        backend_version: token.backend_version,
81        expired: token
82            .expires_at_millis
83            .is_some_and(|expires_at| expires_at <= Utc::now().timestamp_millis()),
84    })
85}
86
87/// Maximum value size in bytes for KV storage (24 KiB = 24,576 bytes)
88///
89/// This limit ensures compatibility across all KV backends, accounting for encoding overhead:
90/// - **AWS DynamoDB**: 400KB item limit (much higher, not constraining)
91/// - **GCP Firestore**: 1MiB document limit (much higher, not constraining)  
92/// - **Azure Table Storage**: 64KB UTF-16 string limit, accounting for base64 + UTF-16 encoding
93///
94/// The 24KB limit accounts for Azure Table Storage's most restrictive constraint:
95/// - 24KB raw data → ~32KB base64 → ~64KB UTF-16, fitting within Azure's 64KB limit
96/// - Still supports reasonably sized data structures and JSON payloads
97/// - Ensures fast network transfer and low latency
98/// - Maintains consistent behavior across all cloud providers
99///
100/// Applications needing larger values should consider:
101/// 1. Compressing data before storage (e.g., gzip JSON)
102/// 2. Splitting data across multiple keys with a common prefix
103/// 3. Using the Storage API for large objects (designed for multi-MB/GB files)
104pub const MAX_VALUE_BYTES: usize = 24_576; // 24 KiB
105
106/// Maximum key size in bytes (512 bytes)
107///
108/// This is a safe floor across all KV backends:
109/// - **AWS DynamoDB**: Sort Key ≤ 1024 bytes  
110/// - **GCP Firestore**: Document ID ≤ 1500 bytes
111/// - **Azure Table Storage**: RowKey ≤ 1024 bytes
112///
113/// The 512-byte limit ensures:
114/// - Universal compatibility across all backends
115/// - Efficient indexing and query performance
116/// - Reasonable prefix scanning capabilities
117/// - Safe URL encoding when needed for REST APIs
118pub const MAX_KEY_BYTES: usize = 512;
119
120/// Global key validation for all KV providers
121///
122/// This ensures consistent behavior across all backends by using the most restrictive
123/// character set that works universally:
124///
125/// **Allowed characters**: `a-z A-Z 0-9 - _ : .`
126///
127/// **Rationale for restrictions**:
128/// - **Forward slash (`/`)**: Disallowed in Azure Table Storage PartitionKey/RowKey
129/// - **Backslash (`\`)**: Disallowed in Azure Table Storage, problematic in URLs
130/// - **Hash (`#`)**: Disallowed in Azure Table Storage, has special meaning in URLs
131/// - **Question mark (`?`)**: Disallowed in Azure Table Storage, query parameter separator
132/// - **Control characters**: Disallowed in Azure Table Storage, unsafe for transmission
133/// - **Space and other special chars**: Can cause encoding issues across backends
134///
135/// **Platform-specific notes**:
136/// - **AWS DynamoDB**: More permissive, but we follow the global restriction
137/// - **GCP Firestore**: More permissive, but we follow the global restriction  
138/// - **Azure Table Storage**: Most restrictive, sets the global standard
139pub fn validate_key(key: &str) -> crate::error::Result<()> {
140    use crate::error::ErrorData;
141    use alien_error::AlienError;
142
143    if key.is_empty() {
144        return Err(AlienError::new(ErrorData::InvalidInput {
145            operation_context: "KV key validation".to_string(),
146            details: "Key cannot be empty".to_string(),
147            field_name: Some("key".to_string()),
148        }));
149    }
150
151    if key.len() > MAX_KEY_BYTES {
152        return Err(AlienError::new(ErrorData::InvalidInput {
153            operation_context: "KV key validation".to_string(),
154            details: format!("Key exceeds {} bytes", MAX_KEY_BYTES),
155            field_name: Some("key".to_string()),
156        }));
157    }
158
159    // Global character set: most restrictive common denominator across all providers
160    // Specifically excludes: / \ # ? and control characters (Azure Table Storage restrictions)
161    if !key.chars().all(|c| {
162        matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | ':' | '.') && !c.is_control()
163    }) {
164        return Err(AlienError::new(ErrorData::InvalidInput {
165            operation_context: "KV key validation".to_string(),
166            details: "Key contains invalid characters. Allowed: a-z A-Z 0-9 - _ : . (no spaces, slashes, or special characters)".to_string(),
167            field_name: Some("key".to_string()),
168        }));
169    }
170
171    Ok(())
172}
173
174/// Global value validation for all KV providers
175pub fn validate_value(value: &[u8]) -> crate::error::Result<()> {
176    use crate::error::ErrorData;
177    use alien_error::AlienError;
178
179    if value.len() > MAX_VALUE_BYTES {
180        return Err(AlienError::new(ErrorData::InvalidInput {
181            operation_context: "KV value validation".to_string(),
182            details: format!("Value exceeds {} bytes", MAX_VALUE_BYTES),
183            field_name: Some("value".to_string()),
184        }));
185    }
186
187    Ok(())
188}
189
190#[cfg(feature = "aws")]
191pub mod aws_dynamodb;
192#[cfg(feature = "azure")]
193pub mod azure_table_storage;
194#[cfg(feature = "gcp")]
195pub mod gcp_firestore;
196#[cfg(feature = "local")]
197pub mod local;
198
199#[cfg(feature = "aws")]
200pub use aws_dynamodb::AwsDynamodbKv;
201#[cfg(feature = "azure")]
202pub use azure_table_storage::AzureTableStorageKv;
203#[cfg(feature = "gcp")]
204pub use gcp_firestore::GcpFirestoreKv;
205#[cfg(feature = "local")]
206pub use local::LocalKv;