use serde_json::Value;
pub fn has_image_source(json: &Value) -> bool {
json.get("image").is_some()
|| json.get("build").is_some()
|| json.get("dockerComposeFile").is_some()
}
pub fn uses_latest_tag(image: &str) -> bool {
image.ends_with(":latest")
}
pub fn is_absolute_path(path: &str) -> bool {
path.starts_with('/')
}
pub fn is_empty_string(s: &str) -> bool {
s.trim().is_empty()
}
pub fn is_non_empty_string(value: &Value) -> bool {
value.as_str().is_some_and(|s| !s.trim().is_empty())
}
pub fn is_valid_lifecycle_command(value: &Value) -> bool {
match value {
Value::String(s) => !s.trim().is_empty(),
Value::Array(_) => true,
Value::Object(_) => true,
_ => false,
}
}
pub fn is_valid_feature_reference(feature: &str) -> bool {
feature.starts_with("ghcr.io/")
|| feature.starts_with("./")
|| feature.starts_with("../")
|| feature.contains('/')
}
pub fn has_duplicate_extensions(extensions: &[String]) -> bool {
let mut seen = std::collections::HashSet::new();
for ext in extensions {
let lower = ext.to_lowercase();
if !seen.insert(lower) {
return true;
}
}
false
}
pub fn find_duplicate_extensions(extensions: &[String]) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for ext in extensions {
let lower = ext.to_lowercase();
if !seen.insert(lower.clone()) {
duplicates.push(ext.clone());
}
}
duplicates
}
pub fn is_valid_port(port: u64) -> bool {
(1..=65535).contains(&port)
}
pub fn is_valid_env_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
&& name
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase() || c == '_')
}
pub fn has_remote_user(json: &Value) -> bool {
json.get("remoteUser").is_some()
}
pub fn has_name(json: &Value) -> bool {
json.get("name").is_some()
}
pub fn has_volume_mounts(mounts: &[String]) -> bool {
mounts.iter().any(|m| m.starts_with("type=volume"))
}
pub fn is_privileged(json: &Value) -> bool {
json.get("privileged")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_has_image_source_image() {
let json = json!({"image": "ubuntu"});
assert!(has_image_source(&json));
}
#[test]
fn test_has_image_source_build() {
let json = json!({"build": {"dockerfile": "Dockerfile"}});
assert!(has_image_source(&json));
}
#[test]
fn test_has_image_source_compose() {
let json = json!({"dockerComposeFile": "docker-compose.yml"});
assert!(has_image_source(&json));
}
#[test]
fn test_has_image_source_none() {
let json = json!({"name": "test"});
assert!(!has_image_source(&json));
}
#[test]
fn test_uses_latest_tag_true() {
assert!(uses_latest_tag("ubuntu:latest"));
assert!(uses_latest_tag(
"mcr.microsoft.com/devcontainers/base:latest"
));
}
#[test]
fn test_uses_latest_tag_false() {
assert!(!uses_latest_tag("ubuntu:22.04"));
assert!(!uses_latest_tag("ubuntu"));
}
#[test]
fn test_is_absolute_path_true() {
assert!(is_absolute_path("/home/user/Dockerfile"));
assert!(is_absolute_path("/Dockerfile"));
}
#[test]
fn test_is_absolute_path_false() {
assert!(!is_absolute_path("./Dockerfile"));
assert!(!is_absolute_path("Dockerfile"));
}
#[test]
fn test_is_empty_string_true() {
assert!(is_empty_string(""));
assert!(is_empty_string(" "));
assert!(is_empty_string("\t\n"));
}
#[test]
fn test_is_empty_string_false() {
assert!(!is_empty_string("hello"));
assert!(!is_empty_string(" x "));
}
#[test]
fn test_is_non_empty_string_true() {
assert!(is_non_empty_string(&json!("hello")));
}
#[test]
fn test_is_non_empty_string_false() {
assert!(!is_non_empty_string(&json!("")));
assert!(!is_non_empty_string(&json!(" ")));
assert!(!is_non_empty_string(&json!(123)));
}
#[test]
fn test_is_valid_lifecycle_command_string() {
assert!(is_valid_lifecycle_command(&json!("npm install")));
assert!(!is_valid_lifecycle_command(&json!("")));
}
#[test]
fn test_is_valid_lifecycle_command_array() {
assert!(is_valid_lifecycle_command(&json!(["npm", "install"])));
}
#[test]
fn test_is_valid_lifecycle_command_object() {
assert!(is_valid_lifecycle_command(
&json!({"install": "npm install"})
));
}
#[test]
fn test_is_valid_lifecycle_command_invalid() {
assert!(!is_valid_lifecycle_command(&json!(123)));
assert!(!is_valid_lifecycle_command(&json!(null)));
}
#[test]
fn test_is_valid_feature_reference_ghcr() {
assert!(is_valid_feature_reference(
"ghcr.io/devcontainers/features/node:1"
));
}
#[test]
fn test_is_valid_feature_reference_local() {
assert!(is_valid_feature_reference("./local-feature"));
assert!(is_valid_feature_reference("../shared-feature"));
}
#[test]
fn test_is_valid_feature_reference_with_slash() {
assert!(is_valid_feature_reference("owner/repo/feature"));
}
#[test]
fn test_has_duplicate_extensions_true() {
let exts = vec!["ms-python.python".into(), "MS-Python.Python".into()];
assert!(has_duplicate_extensions(&exts));
}
#[test]
fn test_has_duplicate_extensions_false() {
let exts = vec!["ms-python.python".into(), "rust-lang.rust-analyzer".into()];
assert!(!has_duplicate_extensions(&exts));
}
#[test]
fn test_find_duplicate_extensions() {
let exts = vec!["a".into(), "b".into(), "A".into()];
let dups = find_duplicate_extensions(&exts);
assert_eq!(dups, vec!["A"]);
}
#[test]
fn test_is_valid_port_true() {
assert!(is_valid_port(80));
assert!(is_valid_port(443));
assert!(is_valid_port(1));
assert!(is_valid_port(65535));
}
#[test]
fn test_is_valid_port_false() {
assert!(!is_valid_port(0));
assert!(!is_valid_port(65536));
}
#[test]
fn test_is_valid_env_name_true() {
assert!(is_valid_env_name("HOME"));
assert!(is_valid_env_name("MY_VAR"));
assert!(is_valid_env_name("_PRIVATE"));
}
#[test]
fn test_is_valid_env_name_false() {
assert!(!is_valid_env_name(""));
assert!(!is_valid_env_name("myVar")); assert!(!is_valid_env_name("123VAR")); }
#[test]
fn test_has_remote_user_true() {
let json = json!({"remoteUser": "vscode"});
assert!(has_remote_user(&json));
}
#[test]
fn test_has_remote_user_false() {
let json = json!({"name": "test"});
assert!(!has_remote_user(&json));
}
#[test]
fn test_has_name_true() {
let json = json!({"name": "My Container"});
assert!(has_name(&json));
}
#[test]
fn test_has_name_false() {
let json = json!({"image": "ubuntu"});
assert!(!has_name(&json));
}
#[test]
fn test_has_volume_mounts_true() {
let mounts = vec!["type=volume,source=mydata,target=/data".into()];
assert!(has_volume_mounts(&mounts));
}
#[test]
fn test_has_volume_mounts_false() {
let mounts = vec!["type=bind,source=./,target=/workspace".into()];
assert!(!has_volume_mounts(&mounts));
let empty: Vec<String> = vec![];
assert!(!has_volume_mounts(&empty));
}
#[test]
fn test_is_privileged_true() {
let json = json!({"privileged": true});
assert!(is_privileged(&json));
}
#[test]
fn test_is_privileged_false() {
let json = json!({"privileged": false});
assert!(!is_privileged(&json));
let json2 = json!({"name": "test"});
assert!(!is_privileged(&json2));
}
}