use crate::error::SledoViewError;
use anyhow::Result;
use regex::Regex;
use sled::{Db, Tree};
use std::path::Path;
pub struct SledViewer {
db: Db,
selected_tree: Option<String>,
}
impl SledViewer {
pub fn new(path: &Path) -> Result<Self> {
let db = sled::open(path).map_err(|e| {
if is_sled_lock_error(&e) {
SledoViewError::DatabaseLocked {
path: path.display().to_string(),
}
.into()
} else {
anyhow::Error::from(e)
}
})?;
Ok(Self {
db,
selected_tree: None,
})
}
pub fn count(&self) -> Result<usize> {
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
Ok(tree.len())
} else {
Ok(self.db.len())
}
}
pub fn list_keys(&self, pattern: &str, is_regex: bool) -> Result<Vec<String>> {
let mut keys = Vec::new();
let regex = create_regex(pattern, is_regex)?;
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
for result in &tree {
let (key, _) = result?;
let key_str = String::from_utf8_lossy(&key);
if regex.is_match(&key_str) {
keys.push(key_str.to_string());
}
}
} else {
for result in self.db.iter() {
let (key, _) = result?;
let key_str = String::from_utf8_lossy(&key);
if regex.is_match(&key_str) {
keys.push(key_str.to_string());
}
}
}
keys.sort();
Ok(keys)
}
pub fn list_keys_raw(&self, pattern: &str, is_regex: bool) -> Result<Vec<Vec<u8>>> {
let mut keys: Vec<Vec<u8>> = Vec::new();
let regex = create_regex(pattern, is_regex)?;
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
for result in &tree {
let (key, _) = result?;
let key_str = String::from_utf8_lossy(&key);
if regex.is_match(&key_str) {
keys.push(key.to_vec());
}
}
} else {
for result in self.db.iter() {
let (key, _) = result?;
let key_str = String::from_utf8_lossy(&key);
if regex.is_match(&key_str) {
keys.push(key.to_vec());
}
}
}
keys.sort();
Ok(keys)
}
pub fn get_key(&self, key: &str) -> Result<KeyInfo> {
let key_bytes = key.as_bytes();
let value_opt = if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
tree.get(key_bytes)?
} else {
self.db.get(key_bytes)?
};
if let Some(value) = value_opt {
let value_str = String::from_utf8_lossy(&value);
let size = value.len();
Ok(KeyInfo {
key: key.to_string(),
value: value_str.to_string(),
size,
is_utf8: String::from_utf8(value.to_vec()).is_ok(),
})
} else {
Err(SledoViewError::KeyNotFound {
key: key.to_string(),
}
.into())
}
}
pub fn get_key_bytes(&self, key_bytes: &[u8]) -> Result<KeyInfo> {
let value_opt = if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
tree.get(key_bytes)?
} else {
self.db.get(key_bytes)?
};
match value_opt {
Some(value) => {
let is_utf8 = std::str::from_utf8(&value).is_ok();
let value_str = String::from_utf8_lossy(&value).to_string();
let size = value.len();
Ok(KeyInfo {
key: format_key_bytes_full(key_bytes),
value: value_str,
size,
is_utf8,
})
}
None => Err(SledoViewError::KeyNotFound {
key: format_key_bytes(key_bytes),
}
.into()),
}
}
pub fn find_key_by_hex_suffix(&self, suffix: &str) -> Result<Option<KeyInfo>> {
let suffix_upper = suffix.to_uppercase();
let try_match = |key: &[u8], value: &sled::IVec| -> Option<KeyInfo> {
if std::str::from_utf8(key).is_ok() {
return None;
}
let hex: String = key.iter().map(|b| format!("{:02X}", b)).collect();
if hex.ends_with(&suffix_upper) {
let is_utf8 = std::str::from_utf8(value).is_ok();
let value_str = String::from_utf8_lossy(value).to_string();
Some(KeyInfo {
key: format_key_bytes_full(key),
value: value_str,
size: value.len(),
is_utf8,
})
} else {
None
}
};
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
for result in &tree {
let (key, value) = result?;
if let Some(info) = try_match(&key, &value) {
return Ok(Some(info));
}
}
} else {
for result in self.db.iter() {
let (key, value) = result?;
if let Some(info) = try_match(&key, &value) {
return Ok(Some(info));
}
}
}
Ok(None)
}
pub fn search_values(&self, pattern: &str, is_regex: bool) -> Result<Vec<KeyValuePair>> {
let mut results = Vec::new();
let regex = create_regex(pattern, is_regex)?;
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
for result in &tree {
let (key, value) = result?;
let value_str = String::from_utf8_lossy(&value);
if regex.is_match(&value_str) {
results.push(KeyValuePair {
key: format_key_bytes(&key),
value: value_str.to_string(),
});
}
}
} else {
for result in self.db.iter() {
let (key, value) = result?;
let value_str = String::from_utf8_lossy(&value);
if regex.is_match(&value_str) {
results.push(KeyValuePair {
key: format_key_bytes(&key),
value: value_str.to_string(),
});
}
}
}
results.sort_by(|a, b| a.key.cmp(&b.key));
Ok(results)
}
pub fn set_key(&self, key: &str, value: &str) -> Result<()> {
if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
tree.insert(key.as_bytes(), value.as_bytes())?;
tree.flush()?;
} else {
self.db.insert(key.as_bytes(), value.as_bytes())?;
self.db.flush()?;
}
Ok(())
}
pub fn delete_key(&self, key: &str) -> Result<bool> {
let existed = if let Some(tree_name) = &self.selected_tree {
let tree = self.get_tree(tree_name)?;
let existed = tree.remove(key.as_bytes())?.is_some();
tree.flush()?;
existed
} else {
let existed = self.db.remove(key.as_bytes())?.is_some();
self.db.flush()?;
existed
};
Ok(existed)
}
#[must_use]
pub fn is_writable(&self) -> bool {
if let Some(tree_name) = &self.selected_tree {
if let Ok(tree) = self.get_tree(tree_name) {
match tree.insert(b"__sledoview_test__", b"test") {
Ok(_) => {
let _ = tree.remove(b"__sledoview_test__");
let _ = tree.flush();
true
}
Err(_) => false,
}
} else {
false
}
} else {
match self.db.insert(b"__sledoview_test__", b"test") {
Ok(_) => {
let _ = self.db.remove(b"__sledoview_test__");
let _ = self.db.flush();
true
}
Err(_) => false,
}
}
}
pub fn list_trees(&self, pattern: &str, is_regex: bool) -> Result<Vec<String>, SledoViewError> {
let regex = create_regex(pattern, is_regex)?;
let mut tree_names = Vec::new();
let all_trees = self.db.tree_names();
for tree_name_bytes in all_trees {
let tree_name = String::from_utf8_lossy(&tree_name_bytes).to_string();
if tree_name.is_empty() || tree_name == "__sled__default" {
continue;
}
if regex.is_match(&tree_name) {
tree_names.push(tree_name);
}
}
tree_names.sort();
Ok(tree_names)
}
pub fn select_tree(&mut self, tree_name: &str) -> Result<()> {
let _ = self.get_tree(tree_name)?;
self.selected_tree = Some(tree_name.to_string());
Ok(())
}
#[allow(clippy::unnecessary_wraps)] pub fn unselect_tree(&mut self) -> Result<bool> {
let was_selected = self.selected_tree.is_some();
self.selected_tree = None;
Ok(was_selected)
}
#[must_use]
pub fn get_selected_tree(&self) -> Option<&String> {
self.selected_tree.as_ref()
}
fn get_tree(&self, name: &str) -> Result<Tree> {
self.db.open_tree(name.as_bytes()).map_err(|e| {
SledoViewError::TreeOperation {
message: format!("Failed to open tree '{name}': {e}"),
}
.into()
})
}
}
#[derive(Debug)]
pub struct KeyInfo {
pub key: String,
pub value: String,
pub size: usize,
pub is_utf8: bool,
}
#[derive(Debug)]
pub struct KeyValuePair {
pub key: String,
pub value: String,
}
#[must_use]
pub fn format_key_bytes(bytes: &[u8]) -> String {
if let Ok(s) = std::str::from_utf8(bytes) {
return s.to_string();
}
let hex: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
if hex.len() > 32 {
format!("{}....{}", &hex[..8], &hex[hex.len() - 8..])
} else {
hex
}
}
#[must_use]
pub fn format_key_bytes_full(bytes: &[u8]) -> String {
if let Ok(s) = std::str::from_utf8(bytes) {
return s.to_string();
}
bytes.iter().map(|b| format!("{b:02X}")).collect()
}
fn is_sled_lock_error(err: &sled::Error) -> bool {
if let sled::Error::Io(io_err) = err {
match io_err.kind() {
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::WouldBlock => return true,
std::io::ErrorKind::Other => {
if let Some(os_code) = io_err.raw_os_error() {
if os_code == 32 || os_code == 33 {
return true;
}
}
}
_ => {}
}
if io_err.to_string().to_lowercase().contains("lock") {
return true;
}
}
err.to_string().to_lowercase().contains("lock")
}
fn glob_to_regex(pattern: &str) -> String {
let mut regex = String::new();
regex.push('^');
for ch in pattern.chars() {
match ch {
'*' => regex.push_str(".*"),
'?' => regex.push('.'),
'[' => regex.push('['),
']' => regex.push(']'),
'\\' => regex.push_str("\\\\"),
'^' => regex.push_str("\\^"),
'$' => regex.push_str("\\$"),
'.' => regex.push_str("\\."),
'|' => regex.push_str("\\|"),
'+' => regex.push_str("\\+"),
'(' => regex.push_str("\\("),
')' => regex.push_str("\\)"),
'{' => regex.push_str("\\{"),
'}' => regex.push_str("\\}"),
c => regex.push(c),
}
}
regex.push('$');
regex
}
fn create_regex(pattern: &str, is_regex: bool) -> Result<Regex, SledoViewError> {
if is_regex {
Regex::new(pattern).map_err(|_| SledoViewError::InvalidRegex {
pattern: pattern.to_string(),
})
} else {
let regex_pattern = glob_to_regex(pattern);
Regex::new(®ex_pattern).map_err(|_| SledoViewError::InvalidRegex {
pattern: pattern.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_db() -> TempDir {
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
{
let db = sled::open(temp_dir.path()).expect("Failed to create test database");
db.insert(b"test_key", b"test_value").unwrap();
db.insert(b"another_key", b"another_value").unwrap();
db.flush().unwrap();
}
temp_dir
}
#[test]
fn test_glob_to_regex() {
assert_eq!(glob_to_regex("*"), "^.*$");
assert_eq!(glob_to_regex("test*"), "^test.*$");
assert_eq!(glob_to_regex("*test"), "^.*test$");
assert_eq!(glob_to_regex("test?"), "^test.$");
assert_eq!(glob_to_regex("test.txt"), "^test\\.txt$");
}
#[test]
fn test_sled_viewer_new() {
let temp_dir = create_test_db();
let viewer = SledViewer::new(temp_dir.path());
assert!(viewer.is_ok());
}
#[test]
fn test_key_info_debug() {
let info = KeyInfo {
key: "test".to_string(),
value: "value".to_string(),
size: 5,
is_utf8: true,
};
let debug_str = format!("{:?}", info);
assert!(debug_str.contains("test"));
assert!(debug_str.contains("value"));
}
#[test]
fn test_key_value_pair_debug() {
let pair = KeyValuePair {
key: "test".to_string(),
value: "value".to_string(),
};
let debug_str = format!("{:?}", pair);
assert!(debug_str.contains("test"));
assert!(debug_str.contains("value"));
}
#[test]
fn test_set_key() {
let temp_dir = create_test_db();
let viewer = SledViewer::new(temp_dir.path()).unwrap();
assert!(viewer.set_key("new_key", "new_value").is_ok());
let info = viewer.get_key("new_key").unwrap();
assert_eq!(info.key, "new_key");
assert_eq!(info.value, "new_value");
assert!(viewer.set_key("new_key", "updated_value").is_ok());
let info = viewer.get_key("new_key").unwrap();
assert_eq!(info.value, "updated_value");
}
#[test]
fn test_delete_key() {
let temp_dir = create_test_db();
let viewer = SledViewer::new(temp_dir.path()).unwrap();
viewer.set_key("test_delete", "value").unwrap();
assert!(viewer.get_key("test_delete").is_ok());
let existed = viewer.delete_key("test_delete").unwrap();
assert!(existed);
assert!(viewer.get_key("test_delete").is_err());
let existed = viewer.delete_key("non_existent").unwrap();
assert!(!existed);
}
#[test]
fn test_is_writable() {
let temp_dir = create_test_db();
let viewer = SledViewer::new(temp_dir.path()).unwrap();
assert!(viewer.is_writable());
}
#[test]
fn test_set_with_special_characters() {
let temp_dir = create_test_db();
let viewer = SledViewer::new(temp_dir.path()).unwrap();
assert!(viewer
.set_key("key with spaces", "value with spaces")
.is_ok());
let info = viewer.get_key("key with spaces").unwrap();
assert_eq!(info.value, "value with spaces");
assert!(viewer.set_key("quote_key", "value with \"quotes\"").is_ok());
let info = viewer.get_key("quote_key").unwrap();
assert_eq!(info.value, "value with \"quotes\"");
}
#[test]
fn test_create_tree_db() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let db = sled::open(temp_dir.path())?;
let tree1 = db.open_tree(b"tree1")?;
tree1.insert(b"key1", b"value1")?;
tree1.insert(b"key2", b"value2")?;
let tree2 = db.open_tree(b"tree2")?;
tree2.insert(b"keyA", b"valueA")?;
tree2.insert(b"keyB", b"valueB")?;
db.insert(b"default_key", b"default_value")?;
db.flush()?;
tree1.flush()?;
tree2.flush()?;
Ok(())
}
#[test]
fn test_list_trees() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_list_trees");
{
let db = sled::open(&db_path).unwrap();
let _tree1 = db.open_tree(b"settings").unwrap();
let _tree2 = db.open_tree(b"sessions").unwrap();
let _tree3 = db.open_tree(b"cache").unwrap();
let _tree4 = db.open_tree(b"my_tree_1").unwrap();
let _tree5 = db.open_tree(b"my_tree_2").unwrap();
db.flush().unwrap();
}
let viewer = SledViewer::new(&db_path).unwrap();
let trees = viewer.list_trees("*", false).unwrap();
assert!(trees.contains(&"settings".to_string()));
assert!(trees.contains(&"sessions".to_string()));
assert!(trees.contains(&"cache".to_string()));
assert!(trees.contains(&"my_tree_1".to_string()));
assert!(trees.contains(&"my_tree_2".to_string()));
let trees = viewer.list_trees("my_tree_*", false).unwrap();
assert_eq!(trees.len(), 2);
assert!(trees.contains(&"my_tree_1".to_string()));
assert!(trees.contains(&"my_tree_2".to_string()));
let trees = viewer.list_trees(r"my_tree_\d+", true).unwrap();
assert_eq!(trees.len(), 2);
assert!(trees.contains(&"my_tree_1".to_string()));
assert!(trees.contains(&"my_tree_2".to_string()));
let trees = viewer.list_trees("nonexistent_*", false).unwrap();
assert!(trees.is_empty());
}
#[test]
fn test_tree_selection() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_tree_selection");
{
let db = sled::open(&db_path).unwrap();
let tree = db.open_tree(b"test_tree").unwrap();
tree.insert(b"tree_key", b"tree_value").unwrap();
tree.flush().unwrap();
db.insert(b"default_key", b"default_value").unwrap();
db.flush().unwrap();
}
let mut viewer = SledViewer::new(&db_path).unwrap();
assert!(viewer.get_selected_tree().is_none());
assert!(viewer.select_tree("test_tree").is_ok());
assert_eq!(viewer.get_selected_tree().unwrap(), "test_tree");
let was_selected = viewer.unselect_tree().unwrap();
assert!(was_selected);
assert!(viewer.get_selected_tree().is_none());
let was_selected = viewer.unselect_tree().unwrap();
assert!(!was_selected);
}
#[test]
fn test_tree_operations_with_selection() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_tree_operations");
{
let db = sled::open(&db_path).unwrap();
let tree1 = db.open_tree(b"tree1").unwrap();
tree1.insert(b"key1", b"tree1_value1").unwrap();
tree1.insert(b"key2", b"tree1_value2").unwrap();
let tree2 = db.open_tree(b"tree2").unwrap();
tree2.insert(b"key1", b"tree2_value1").unwrap();
tree2.insert(b"keyA", b"tree2_valueA").unwrap();
db.insert(b"key1", b"default_value1").unwrap();
db.insert(b"default_key", b"default_value").unwrap();
db.flush().unwrap();
tree1.flush().unwrap();
tree2.flush().unwrap();
}
let mut viewer = SledViewer::new(&db_path).unwrap();
let keys = viewer.list_keys("*", false).unwrap();
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"default_key".to_string()));
assert_eq!(keys.len(), 2);
let info = viewer.get_key("key1").unwrap();
assert_eq!(info.value, "default_value1");
viewer.select_tree("tree1").unwrap();
let keys = viewer.list_keys("*", false).unwrap();
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
assert_eq!(keys.len(), 2);
let info = viewer.get_key("key1").unwrap();
assert_eq!(info.value, "tree1_value1");
assert_eq!(viewer.count().unwrap(), 2);
viewer.select_tree("tree2").unwrap();
let keys = viewer.list_keys("*", false).unwrap();
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"keyA".to_string()));
assert_eq!(keys.len(), 2);
let info = viewer.get_key("key1").unwrap();
assert_eq!(info.value, "tree2_value1");
assert_eq!(viewer.count().unwrap(), 2);
}
#[test]
fn test_tree_set_and_delete_operations() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_tree_set_delete");
{
let db = sled::open(&db_path).unwrap();
let _tree = db.open_tree(b"test_tree").unwrap();
db.flush().unwrap();
}
let mut viewer = SledViewer::new(&db_path).unwrap();
viewer.set_key("default_key", "default_value").unwrap();
let info = viewer.get_key("default_key").unwrap();
assert_eq!(info.value, "default_value");
viewer.select_tree("test_tree").unwrap();
viewer.set_key("tree_key", "tree_value").unwrap();
let info = viewer.get_key("tree_key").unwrap();
assert_eq!(info.value, "tree_value");
viewer.unselect_tree().unwrap();
assert!(viewer.get_key("tree_key").is_err());
let info = viewer.get_key("default_key").unwrap();
assert_eq!(info.value, "default_value");
viewer.select_tree("test_tree").unwrap();
let existed = viewer.delete_key("tree_key").unwrap();
assert!(existed);
assert!(viewer.get_key("tree_key").is_err());
let existed = viewer.delete_key("non_existent").unwrap();
assert!(!existed);
}
#[test]
fn test_tree_search_operations() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_tree_search");
{
let db = sled::open(&db_path).unwrap();
let tree = db.open_tree(b"search_tree").unwrap();
tree.insert(b"user_001", b"John Doe").unwrap();
tree.insert(b"user_002", b"Jane Smith").unwrap();
tree.insert(b"admin_001", b"Admin User").unwrap();
db.insert(b"user_001", b"Default John").unwrap();
db.insert(b"config_001", b"Config Value").unwrap();
db.flush().unwrap();
tree.flush().unwrap();
}
let mut viewer = SledViewer::new(&db_path).unwrap();
let results = viewer.search_values("*John*", false).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].key, "user_001");
assert_eq!(results[0].value, "Default John");
viewer.select_tree("search_tree").unwrap();
let results = viewer.search_values("*John*", false).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].key, "user_001");
assert_eq!(results[0].value, "John Doe");
let results = viewer.search_values("*Smith", false).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].value, "Jane Smith");
let keys = viewer.list_keys("user_*", false).unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"user_001".to_string()));
assert!(keys.contains(&"user_002".to_string()));
}
#[test]
fn test_tree_errors() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("test_tree_errors");
let mut viewer = SledViewer::new(&db_path).unwrap();
assert!(viewer.select_tree("nonexistent_tree").is_ok());
assert_eq!(viewer.get_selected_tree().unwrap(), "nonexistent_tree");
let keys = viewer.list_keys("*", false).unwrap();
assert!(keys.is_empty());
let count = viewer.count().unwrap();
assert_eq!(count, 0);
}
}