fn parse_ccs_profiles_from_config_yaml(content: &str) -> std::collections::HashMap<String, String> {
let lines: Vec<&str> = content.lines().collect();
process_lines(&lines, 0, ParserState::default(), Vec::new())
.into_iter()
.collect()
}
fn process_lines(
lines: &[&str],
index: usize,
state: ParserState,
entries: Vec<(String, String)>,
) -> Vec<(String, String)> {
if index >= lines.len() {
return entries;
}
let line = lines[index];
let trimmed = line.trim_end();
if trimmed.trim().is_empty() {
return process_lines(lines, index + 1, state, entries);
}
let trimmed_start = trimmed.trim_start();
if trimmed_start.starts_with('#') {
return process_lines(lines, index + 1, state, entries);
}
let indent = trimmed.len().saturating_sub(trimmed_start.len());
if !state.in_profiles {
if trimmed_start == "profiles:" {
let new_state = ParserState {
in_profiles: true,
profiles_indent: indent,
..ParserState::default()
};
return process_lines(lines, index + 1, new_state, entries);
}
return process_lines(lines, index + 1, state, entries);
}
if indent <= state.profiles_indent {
return process_lines(lines, index + 1, ParserState::default(), entries);
}
if state.current_profile.is_none() {
if let Some((name, rest)) = trimmed_start.split_once(':') {
let profile_name = name.trim().to_string();
let rest = rest.trim();
let new_profile = Some((profile_name.clone(), indent));
let new_entries = if rest.contains("settings:") {
if let Some(settings) = extract_yaml_inline_settings_value(rest) {
entries.into_iter().chain([(profile_name, settings)]).collect()
} else {
entries
}
} else {
entries
};
let new_state = ParserState {
in_profiles: state.in_profiles,
profiles_indent: state.profiles_indent,
current_profile: new_profile,
};
return process_lines(lines, index + 1, new_state, new_entries);
}
return process_lines(lines, index + 1, state, entries);
}
if let Some((profile_name, profile_indent)) = state.current_profile.as_ref() {
if indent <= *profile_indent {
let new_state = ParserState {
in_profiles: state.in_profiles,
profiles_indent: state.profiles_indent,
current_profile: None,
};
return process_lines(lines, index + 1, new_state, entries);
}
if let Some(value) = trimmed_start.strip_prefix("settings:") {
let settings = unquote_yaml_scalar(value.trim());
if !settings.is_empty() {
let new_entries: Vec<_> = entries
.into_iter()
.chain([(profile_name.clone(), settings)])
.collect();
return process_lines(lines, index + 1, state, new_entries);
}
}
}
process_lines(lines, index + 1, state, entries)
}
#[derive(Default)]
struct ParserState {
in_profiles: bool,
profiles_indent: usize,
current_profile: Option<(String, usize)>,
}
fn extract_yaml_inline_settings_value(inline: &str) -> Option<String> {
let needle = "settings:";
let idx = inline.find(needle)?;
let start = idx.checked_add(needle.len())?;
let after = inline[start..].trim_start();
let token = after
.split(',')
.next()
.unwrap_or(after)
.trim()
.trim_end_matches('}')
.trim();
let value = unquote_yaml_scalar(token);
if value.is_empty() {
None
} else {
Some(value)
}
}
fn unquote_yaml_scalar(value: &str) -> String {
let v = value.trim();
if v.len() >= 2
&& ((v.starts_with('"') && v.ends_with('"')) || (v.starts_with('\'') && v.ends_with('\'')))
{
let end_idx = v.len().saturating_sub(1);
let inner = &v[1..end_idx];
inner.replace("\\\"", "\"").replace("\\\\", "\\")
} else {
v.to_string()
}
}
fn load_ccs_profiles_from_config_yaml_with_deps(
env: &dyn CcsEnvironment,
fs: &dyn CcsFilesystem,
) -> Result<std::collections::HashMap<String, String>, CcsEnvVarsError> {
let Some(path) = ccs_config_yaml_path_with_env(env) else {
return Err(CcsEnvVarsError::MissingHomeDir);
};
if !fs.exists(&path) {
return Ok(std::collections::HashMap::new());
}
let content = fs
.read_to_string(&path)
.map_err(|source| CcsEnvVarsError::ReadConfig {
path: path.clone(),
source,
})?;
Ok(parse_ccs_profiles_from_config_yaml(&content))
}
fn resolve_ccs_settings_path_with_deps(
env: &dyn CcsEnvironment,
fs: &dyn CcsFilesystem,
profile: &str,
) -> Result<std::path::PathBuf, CcsEnvVarsError> {
let Some(ccs_dir) = ccs_dir_with_env(env) else {
return Err(CcsEnvVarsError::MissingHomeDir);
};
let yaml_profiles = load_ccs_profiles_from_config_yaml_with_deps(env, fs)?;
if let Some(settings) = yaml_profiles.get(profile) {
if !is_path_safe_for_resolution(settings) {
return Err(CcsEnvVarsError::UnsafeSettingsPath {
path: ccs_dir.join("config.yaml"),
settings_path: settings.clone(),
});
}
return Ok(expand_user_path_with_env(env, settings));
}
let json_profiles = load_ccs_profiles_from_config_json_with_deps(env, fs)?;
if let Some(settings) = json_profiles.get(profile) {
if !is_path_safe_for_resolution(settings) {
return Err(CcsEnvVarsError::UnsafeSettingsPath {
path: ccs_dir.join("config.json"),
settings_path: settings.clone(),
});
}
return Ok(expand_user_path_with_env(env, settings));
}
if is_safe_profile_filename_stem(profile) {
let candidates = [
ccs_dir.join(format!("{profile}.settings.json")),
ccs_dir.join(format!("{profile}.setting.json")),
];
return candidates
.iter()
.find(|c| fs.exists(c))
.map(|c| Ok(c.clone()))
.unwrap_or_else(|| {
Err(CcsEnvVarsError::ProfileNotFound {
profile: profile.to_string(),
ccs_dir,
})
});
}
Err(CcsEnvVarsError::ProfileNotFound {
profile: profile.to_string(),
ccs_dir,
})
}
fn is_absolute_path(path: &str) -> bool {
if path.starts_with('/') {
return true;
}
if cfg!(windows) {
let bytes = path.as_bytes();
if bytes.len() >= 2 {
let first = bytes.first();
let second = bytes.get(1);
return (first == Some(&b'\\') && second == Some(&b'\\'))
|| (first.copied().map(|b| b.is_ascii_alphabetic()).unwrap_or(false) && second == Some(&b':'));
}
}
false
}
fn is_path_safe_for_resolution(path: &str) -> bool {
if is_absolute_path(path) {
return false;
}
if path.contains("..") {
return false;
}
if path.contains('\0') {
return false;
}
true
}
fn expand_user_path_with_env(env: &dyn CcsEnvironment, path: &str) -> std::path::PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = ccs_home_dir_with_env(env) {
return home.join(rest);
}
}
if let Some(ccs_dir) = ccs_dir_with_env(env) {
if !is_absolute_path(path) {
return ccs_dir.join(path);
}
}
std::path::PathBuf::from(path)
}
fn find_env_object(
json: &serde_json::Value,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
json.as_object()?.get("env")?.as_object()
}