alien_bindings/providers/kv/
mod.rs1#[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
87pub const MAX_VALUE_BYTES: usize = 24_576; pub const MAX_KEY_BYTES: usize = 512;
119
120pub 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 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
174pub 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;