<commit_before>use super::{
    external_libraries::{read_external_libraries, ExternalLibrary},
    gobjects, WorkMode,
};
use crate::{
    config::error::TomlHelper,
    git::repo_hash,
    library::{self, Library},
    nameutil::set_crate_name_overrides,
    version::Version,
};
use log::warn;
use std::{
    collections::HashMap,
    fs,
    path::{Path, PathBuf},
    str::FromStr,
};

#[derive(Debug)]
pub struct GirVersion {
    pub gir_dir: PathBuf,
    hash: Option<String>,
}

impl GirVersion {
    pub fn get_hash(&self) -> &str {
        self.hash.as_deref().unwrap_or("???")
    }

    pub fn get_repository_url(&self) -> Option<&str> {
        match self.gir_dir.file_name() {
            Some(r) if r == "gir-files" => Some("https://github.com/gtk-rs/gir-files"),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub struct Config {
    pub work_mode: WorkMode,
    pub girs_dirs: Vec<PathBuf>,
    // Version in girs_dirs, detected by git
    pub girs_version: Vec<GirVersion>,
    pub library_name: String,
    pub library_version: String,
    pub target_path: PathBuf,
    /// Path where files generated in normal and sys mode
    pub auto_path: PathBuf,
    pub doc_target_path: PathBuf,
    pub external_libraries: Vec<ExternalLibrary>,
    pub objects: gobjects::GObjects,
    pub min_cfg_version: Version,
    pub make_backup: bool,
    pub generate_safety_asserts: bool,
    pub deprecate_by_min_version: bool,
    pub show_statistics: bool,
    pub concurrency: library::Concurrency,
    pub single_version_file: Option<PathBuf>,
    pub generate_display_trait: bool,
    pub trust_return_value_nullability: bool,
    pub docs_rs_features: Vec<String>,
    pub disable_format: bool,
    pub split_build_rs: bool,
    pub extra_versions: Vec<Version>,
    pub lib_version_overrides: HashMap<Version, Version>,
    pub feature_dependencies: HashMap<Version, Vec<String>>,
    pub dox_feature_dependencies: Vec<String>,
}

impl Config {
    pub fn new<'a, S, W>(
        config_file: S,
        work_mode: W,
        girs_dirs: &[String],
        library_name: S,
        library_version: S,
        target_path: S,
        doc_target_path: S,
        make_backup: bool,
        show_statistics: bool,
        disable_format: bool,
    ) -> Result<Config, String>
    where
        S: Into<Option<&'a str>>,
        W: Into<Option<WorkMode>>,
    {
        let config_file: PathBuf = match config_file.into() {
            Some("") | None => "Gir.toml",
            Some(a) => a,
        }
        .into();

        let config_dir = match config_file.parent() {
            Some(path) => path.into(),
            None => PathBuf::new(),
        };

        let toml = match read_toml(&config_file) {
            Ok(toml) => toml,
            Err(e) => {
                return Err(format!(
                    "Error while reading \"{}\": {}",
                    config_file.display(),
                    e
                ))
            }
        };

        let overrides = read_crate_name_overrides(&toml);
        if !overrides.is_empty() {
            set_crate_name_overrides(overrides);
        }

        let work_mode = match work_mode.into() {
            Some(w) => w,
            None => {
                let s = match toml.lookup_str("options.work_mode", "No options.work_mode") {
                    Ok(s) => s,
                    Err(e) => {
                        return Err(format!(
                            "Invalid toml file \"{}\": {}",
                            config_file.display(),
                            e
                        ))
                    }
                };
                WorkMode::from_str(s)?
            }
        };

        let mut girs_dirs: Vec<PathBuf> = girs_dirs
            .iter()
            .filter(|x| !x.is_empty())
            .map(|x| PathBuf::from(&x))
            .collect();
        if girs_dirs.is_empty() {
            let dirs =
                toml.lookup_vec("options.girs_directories", "No options.girs_directories")?;
            for dir in dirs {
                let dir = dir.as_str().ok_or_else(|| {
                    "options.girs_dirs expected to be array of string".to_string()
                })?;
                girs_dirs.push(config_dir.join(dir));
            }
        }
        let mut girs_version = girs_dirs
            .iter()
            .map(|d| GirVersion {
                gir_dir: d.clone(),
                hash: repo_hash(d),
            })
            .collect::<Vec<_>>();
        girs_version.sort_by(|a, b| a.gir_dir.partial_cmp(&b.gir_dir).unwrap());

        let (library_name, library_version) = match (library_name.into(), library_version.into()) {
            (Some(""), Some("")) | (None, None) => (
                toml.lookup_str("options.library", "No options.library")?
                    .to_owned(),
                toml.lookup_str("options.version", "No options.version")?
                    .to_owned(),
            ),
            (Some(""), Some(_)) | (Some(_), Some("")) | (None, Some(_)) | (Some(_), None) => {
                return Err("Library and version can not be specified separately".to_owned())
            }<commit_msg>config: "Canonicalize" ../ away from relative git paths<commit_after>use super::{
    external_libraries::{read_external_libraries, ExternalLibrary},
    gobjects, WorkMode,
};
use crate::{
    config::error::TomlHelper,
    git::repo_hash,
    library::{self, Library},
    nameutil::set_crate_name_overrides,
    version::Version,
};
use log::warn;
use std::{
    collections::HashMap,
    fs,
    path::{Component, Path, PathBuf},
    str::FromStr,
};

/// Performs canonicalization by removing `foo/../` and `./` components
/// from `path`, without hitting the file system. It does not turn relative
/// paths into absolute paths.
fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
    let mut parts: Vec<Component<'_>> = vec![];

    for component in path.as_ref().components() {
        match (component, parts.last()) {
            (Component::CurDir, _) | (Component::ParentDir, Some(Component::RootDir)) => {}
            (Component::ParentDir, None) | (Component::ParentDir, Some(Component::ParentDir)) => {
                parts.push(Component::ParentDir)
            }
            (Component::ParentDir, Some(_)) => {
                parts
                    .pop()
                    .expect("Cannot navigate outside of base directory!");
            }
            (c, _) => parts.push(c),
        }
    }
    parts.iter().collect()
}

#[test]
fn test_normalize_path() {
    assert_eq!(normalize_path("foo/../bar").as_os_str(), "bar");
    assert_eq!(normalize_path("foo/./bar").as_os_str(), "foo/bar");
    assert_eq!(normalize_path("./foo").as_os_str(), "foo");
    assert_eq!(
        normalize_path("foo/../bar/baz/../qux").as_os_str(),
        "bar/qux"
    );
    assert_eq!(
        normalize_path("foo/bar/baz/../../qux").as_os_str(),
        "foo/qux"
    );
    assert_eq!(normalize_path("/foo/../bar").as_os_str(), "/bar");
    assert_eq!(normalize_path("/../bar").as_os_str(), "/bar");
    assert_eq!(normalize_path("foo/../../bar").as_os_str(), "../bar");
}

#[derive(Debug)]
pub struct GirVersion {
    pub gir_dir: PathBuf,
    file_name: Option<String>,
    hash: Option<String>,
}

impl GirVersion {
    fn new(gir_dir: &Path, hash: Option<String>) -> Self {
        Self {
            gir_dir: normalize_path(gir_dir),
            file_name: gir_dir
                .file_name()
                .map(|s| s.to_str().expect("OsStr::to_str failed").to_owned()),
            hash,
        }
    }

    pub fn get_hash(&self) -> &str {
        self.hash.as_deref().unwrap_or("???")
    }

    pub fn get_repository_url(&self) -> Option<&str> {
        match self.file_name {
            Some(ref r) if r == "gir-files" => Some("https://github.com/gtk-rs/gir-files"),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub struct Config {
    pub work_mode: WorkMode,
    pub girs_dirs: Vec<PathBuf>,
    // Version in girs_dirs, detected by git
    pub girs_version: Vec<GirVersion>,
    pub library_name: String,
    pub library_version: String,
    pub target_path: PathBuf,
    /// Path where files generated in normal and sys mode
    pub auto_path: PathBuf,
    pub doc_target_path: PathBuf,
    pub external_libraries: Vec<ExternalLibrary>,
    pub objects: gobjects::GObjects,
    pub min_cfg_version: Version,
    pub make_backup: bool,
    pub generate_safety_asserts: bool,
    pub deprecate_by_min_version: bool,
    pub show_statistics: bool,
    pub concurrency: library::Concurrency,
    pub single_version_file: Option<PathBuf>,
    pub generate_display_trait: bool,
    pub trust_return_value_nullability: bool,
    pub docs_rs_features: Vec<String>,
    pub disable_format: bool,
    pub split_build_rs: bool,
    pub extra_versions: Vec<Version>,
    pub lib_version_overrides: HashMap<Version, Version>,
    pub feature_dependencies: HashMap<Version, Vec<String>>,
    pub dox_feature_dependencies: Vec<String>,
}

impl Config {
    pub fn new<'a, S, W>(
        config_file: S,
        work_mode: W,
        girs_dirs: &[String],
        library_name: S,
        library_version: S,
        target_path: S,
        doc_target_path: S,
        make_backup: bool,
        show_statistics: bool,
        disable_format: bool,
    ) -> Result<Config, String>
    where
        S: Into<Option<&'a str>>,
        W: Into<Option<WorkMode>>,
    {
        let config_file: PathBuf = match config_file.into() {
            Some("") | None => "Gir.toml",
            Some(a) => a,
        }
        .into();

        let config_dir = match config_file.parent() {
            Some(path) => path.into(),
            None => PathBuf::new(),
        };

        let toml = match read_toml(&config_file) {
            Ok(toml) => toml,
            Err(e) => {
                return Err(format!(
                    "Error while reading \"{}\": {}",
                    config_file.display(),
                    e
                ))
            }
        };

        let overrides = read_crate_name_overrides(&toml);
        if !overrides.is_empty() {
            set_crate_name_overrides(overrides);
        }

        let work_mode = match work_mode.into() {
            Some(w) => w,
            None => {
                let s = match toml.lookup_str("options.work_mode", "No options.work_mode") {
                    Ok(s) => s,
                    Err(e) => {
                        return Err(format!(
                            "Invalid toml file \"{}\": {}",
                            config_file.display(),
                            e
                        ))
                    }
                };
                WorkMode::from_str(s)?
            }
        };

        let mut girs_dirs: Vec<PathBuf> = girs_dirs
            .iter()
            .filter(|x| !x.is_empty())
            .map(|x| PathBuf::from(&x))
            .collect();
        if girs_dirs.is_empty() {
            let dirs =
                toml.lookup_vec("options.girs_directories", "No options.girs_directories")?;
            for dir in dirs {
                let dir = dir.as_str().ok_or_else(|| {
                    "options.girs_dirs expected to be array of string".to_string()
                })?;
                girs_dirs.push(config_dir.join(dir));
            }
        }
        let mut girs_version = girs_dirs
            .iter()
            .map(|d| GirVersion::new(d, repo_hash(d)))
            .collect::<Vec<_>>();
        girs_version.sort_by(|a, b| a.gir_dir.partial_cmp(&b.gir_dir).unwrap());

        let (library_name, library_version) = match (library_name.into(), library_version.into()) {
            (Some(""), Some("")) | (None, None) => (
                toml.lookup_str("options.library", "No options.library")?
                    .to_owned(),
                toml.lookup_str("options.version", "No options.version")?
                    .to_owned(),
            ),
            (Some(""), Some(_)) | (Some(_), Some("")) | (None, Some(_)) | (Some(_), None) => {
                return Err("Library and version can not be specified separately".to_owned())
            }
{'fraction_non_alphanumeric': 0.1397155003270111, 'fraction_numerical': 0.0, 'mean_word_length': 1.9057007125890737, 'pattern_counts': {'":': 4, '<': 50, '<?xml version=': 0, '>': 90, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7517423', 'n_tokens_mistral': 3788, 'n_tokens_neox': 3750, 'n_words': 767}