Skip to main content

ploit_malper/db/
content.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::config::config_dir;
5use crate::db::models::sha256_hex;
6use crate::error::Result;
7
8/// Content that exceeds this size is never mirrored back into the database;
9/// it lives only in the content store.
10pub const MAX_DB_CONTENT: usize = 2000;
11
12/// Directory holding all bulk content (full reports, long finding evidence).
13/// Relative paths stored in the database are resolved against this.
14pub fn content_dir() -> PathBuf {
15    config_dir().join("content")
16}
17
18/// Store `content` in the content store, content-addressed by sha256. Returns
19/// `(hash, relative_path)` where relative_path is relative to the config dir.
20pub fn store_content(content: &str) -> Result<(String, String)> {
21    let hash = sha256_hex(content);
22    let dir = content_dir();
23    fs::create_dir_all(&dir)?;
24    let abs = dir.join(&hash);
25    if !abs.exists() {
26        fs::write(&abs, content)?;
27    }
28    Ok((hash.clone(), format!("content/{}", hash)))
29}
30
31/// Resolve a content-store relative path to an absolute filesystem path.
32pub fn resolve_path(rel: &str) -> PathBuf {
33    config_dir().join(rel)
34}
35
36/// Read a content-store file back.
37pub fn read_content(rel: &str) -> Result<String> {
38    Ok(fs::read_to_string(resolve_path(rel))?)
39}
40
41/// Truncate a string to at most `max` characters for storage as a short DB
42/// field; the full content should already be in the content store.
43pub fn short_excerpt(content: &str, max: usize) -> String {
44    if content.chars().count() <= max {
45        return content.to_string();
46    }
47    let mut out: String = content.chars().take(max).collect();
48    out.push('…');
49    out
50}
51
52/// True if the content is "short stuff" that can stay in the database.
53pub fn is_short(content: &str) -> bool {
54    content.len() <= MAX_DB_CONTENT
55}
56
57/// Store long content in the content store and keep only a short excerpt in
58/// the database. Returns `(db_value, detail_path)`; for short content the
59/// value is the full content and `detail_path` is `None`.
60pub fn store_or_excerpt(content: &str) -> Result<(String, Option<String>)> {
61    if is_short(content) {
62        return Ok((content.to_string(), None));
63    }
64    let (_, rel) = store_content(content)?;
65    Ok((short_excerpt(content, MAX_DB_CONTENT), Some(rel)))
66}
67
68/// Ensure a path is a content-store-relative path (sanity check).
69pub fn is_content_rel(path: &Path) -> bool {
70    path.starts_with("content")
71}