/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashSet;
use std::path::Path;
use std::process::Command;

use nodeipc::derive::HasIpc;
use nodeipc::ipc;
use nodeipc::NodeIpc;
use serde::Deserialize;
use serde::Serialize;

use crate::util;

#[derive(Serialize, Deserialize)]
pub struct CommandEnv {
    pub env: Vec<(String, String)>,
    pub cwd: String,
}

#[derive(Serialize, Deserialize)]
pub struct ProcessProps {
    pub pid: u32,
    pub pgid: u32,
    pub groups: Option<Vec<u32>>,
    pub rlimit_nofile: Option<u64>,
}

pub struct Client {
    pub ipc: NodeIpc,
}

pub struct Server<'a> {
    pub ipc: NodeIpc,
    pub run_func: &'a (dyn (Fn(Vec<String>) -> i32) + Send + Sync),
}

#[ipc]
impl Client {
    /// Run a shell command. Return exit code.
    fn system(&self, env: CommandEnv, command: String) -> i32 {
        tracing::debug!("client::system {}", command);
        // Maybe we don't actually need a shell.
        let need_shell = command.contains(|ch| "|&;<>()$`\"' \t\n*?[#~=%".contains(ch)) || {
            let path = Path::new(&command);
            path.is_absolute() && !path.exists()
        };
        let mut cmd = if need_shell {
            if cfg!(windows) {
                let cmd_spec = std::env::var("ComSpec");
                Command::new(cmd_spec.unwrap_or_else(|_| "cmd.exe".to_owned()))
            } else {
                Command::new("/bin/sh")
            }
        } else {
            Command::new(command)
        };
        if need_shell {
            cmd.arg(if cfg!(windows) { "/c" } else { "-c" });
        }

        let CommandEnv { cwd, env } = env;
        cmd.env_clear().envs(env).current_dir(cwd);
        match cmd.status() {
            Ok(v) => match v.code() {
                Some(v) => v,
                None => {
                    #[cfg(unix)]
                    {
                        use std::os::unix::process::ExitStatusExt;
                        return match v.signal() {
                            Some(v) => 128 + v,
                            None => todo!(),
                        };
                    }
                    #[allow(unreachable_code)]
                    128
                }
            },
            Err(_) => 127,
        }
    }
}

#[ipc]
impl Server<'_> {
    /// Report the server process environment.
    fn process_props(&self) -> ProcessProps {
        tracing::debug!("server::process_props");
        let pgid = {
            #[cfg(unix)]
            unsafe {
                libc::getpgid(0) as u32
            }
            #[cfg(not(unix))]
            0u32
        };
        ProcessProps {
            pid: std::process::id() as _,
            pgid,
            groups: util::groups(),
            rlimit_nofile: util::rlimit_nofile(),
        }
    }

    /// Apply the environment. Return `true` on success.
    fn apply_env(&self, env: CommandEnv, umask: Option<u32>) -> bool {
        tracing::debug!("server::apply_env");
        let CommandEnv { cwd, env } = env;
        if std::env::set_current_dir(&cwd).is_err() {
            return false;
        }
        let new_key_set: HashSet<_> = env.iter().map(|(k, _)| k).collect();
        for (k, _) in std::env::vars() {
            if !new_key_set.contains(&k) {
                std::env::remove_var(k);
            }
        }
        for (k, v) in &env {
            std::env::set_var(k, v);
        }
        if let Some(umask) = umask {
            #[cfg(unix)]
            unsafe {
                libc::umask(umask as _);
            }
            let _ = umask;
        }
        true
    }

    /// Run the given main command. Return exit code.
    fn run_command(&self, argv: Vec<String>) -> i32 {
        tracing::debug!("server::run_command {:?}", &argv);
        // To avoid circular dependency, we cannot call hgcommands here.
        // Instead, rely on hgcommands to provide Server::run_func.
        (self.run_func)(argv)
    }
}

impl CommandEnv {
    pub fn current() -> anyhow::Result<Self> {
        let cwd = std::env::current_dir()?
            .to_str()
            .ok_or_else(|| anyhow::format_err!("Current directory is not in UTF-8"))?
            .to_owned();
        // Skip NODE_CHANNEL_FD automatically. The other side likely does not want it.
        let env = Self {
            env: std::env::vars()
                .filter(|(k, _)| k != "NODE_CHANNEL_FD")
                .collect(),
            cwd,
        };
        Ok(env)
    }
}

impl HasIpc for Client {
    fn ipc(&self) -> &NodeIpc {
        &self.ipc
    }
}

impl HasIpc for Server<'_> {
    fn ipc(&self) -> &NodeIpc {
        &self.ipc
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Client-server with the ability to preload content server-side to reduce
//! startup overhead.

pub mod client;
mod ipc;
pub mod server;
mod spawn;
mod util;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::time::Instant;

use nodeipc::derive::Serve;

use crate::ipc::Server;

/// Serve one client.
///
/// Internally, creates and listens to a uds.
///
/// Exits the process when the uds file is removed and no clients
/// are connected.
///
/// Returns if completes serving a client.
pub fn serve_one_client(
    run_func: &(dyn (Fn(Vec<String>) -> i32) + Send + Sync),
) -> anyhow::Result<()> {
    let dir = crate::util::runtime_dir()?;
    tracing::debug!("serving at {}", dir.display());
    let incoming = udsipc::pool::serve(&dir)?;

    let is_uds_alive = incoming.get_is_alive_func();
    let is_waiting = AtomicBool::new(true);
    let start_time = Instant::now();

    thread::scope(|s| {
        // `for ipc in incoming` might block forever waiting for
        // a client. Detect that and exit early.
        s.spawn(|| {
            let idle_timeout = Duration::from_secs(1800);
            let interval = Duration::from_secs(5);
            while is_waiting.load(Ordering::Acquire)
                && start_time.elapsed() < idle_timeout
                && is_uds_alive()
            {
                thread::sleep(interval);
            }
            if is_waiting.load(Ordering::Acquire) {
                tracing::debug!("exiting server due to inactivity");
                std::process::exit(0);
            }
        });

        tracing::debug!("waiting for client connection");
        for ipc in incoming {
            tracing::debug!("got client connection");
            is_waiting.store(false, Ordering::Release);
            if let Err(e) = ipc.recv_stdio() {
                tracing::warn!("failed to get client stdio:\n{:?}", &e);
            } else {
                tracing::debug!("server got client stdio");
                let server = Server { ipc, run_func };
                let _ = server.serve();
            }
            break;
        }
    });

    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fs;
use std::io;
use std::process::Child;
use std::process::Command;

use fs2::FileExt;
use spawn_ext::CommandExt;

use crate::util;

/// Attempt to spawn servers (from a client) so there will be `pool_size`
/// servers running in background.
pub fn spawn_pool(pool_size: usize) -> anyhow::Result<()> {
    let dir = util::runtime_dir()?;
    let spawn_lock = fs::OpenOptions::new()
        .create(true)
        .write(true)
        .open(dir.join("spawn.lock"))?;
    spawn_lock.lock_exclusive()?;

    let existing = udsipc::pool::list_uds_paths(&dir).take(pool_size).count();
    let needed = pool_size.saturating_sub(existing);

    tracing::debug!("spawning {} command servers", needed);
    for _ in 0..needed {
        spawn_one()?;
    }
    Ok(())
}

/// Attempt to spawn one server (from a client).
/// Assume `$0 --spawn-commandserver` is the way to run a command server.
pub fn spawn_one() -> io::Result<Child> {
    let arg0 = std::env::current_exe()?;
    let mut cmd = Command::new(arg0);
    cmd.arg("start-commandserver")
        .current_dir("/")
        // The server will get node channel fd via recv_stdio.
        // They should not have NODE_CHANNEL_FD via env vars.
        .env_remove("NODE_CHANNEL_FD");

    tracing::debug!("spawning a command server");
    if tracing::enabled!(tracing::Level::DEBUG) {
        // Do not silent stderr for easier debugging.
        cmd.spawn()
    } else {
        // Silent stderr.
        cmd.spawn_detached()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Utilities shared for the crate.

use std::fs;
use std::io;
use std::path::PathBuf;

use anyhow::Context;
use fn_error_context::context;
use once_cell::sync::Lazy;

// The socket directory name contains version and identity
// so we can have multiple servers running with different
// identities or versions, and we don't need to check versions
// and invalidate servers manually.
static SOCKET_DIR_NAME: Lazy<String> = Lazy::new(|| {
    let short_version: &str =
        match version::VERSION.rsplit_once(|ch: char| !ch.is_ascii_alphanumeric()) {
            Some((_, rest)) => rest,
            None => version::VERSION,
        };
    let cli_name = identity::default().cli_name();
    format!("{}-cmdserver-{}", cli_name, short_version)
});

/// Create and return a runtime directory intended for uds files.
/// The directory contains `SOCKET_DIR_NAME` in its path.
#[context("Creating a runtime directory")]
pub(crate) fn runtime_dir() -> anyhow::Result<PathBuf> {
    let parent = match dirs::runtime_dir().or_else(dirs::data_local_dir) {
        None => {
            #[allow(unused_mut)]
            let mut dir = std::env::temp_dir();
            #[cfg(unix)]
            {
                // temp_dir() is usually insecure, globally writable on *nix.
                // Try to create a directory with 0o700 permission in it.
                use std::fs::DirBuilder;
                use std::os::unix::fs::DirBuilderExt;

                let mut builder = DirBuilder::new();
                dir = dir.join(format!("uid-{}", unsafe { libc::getuid() }));
                match builder.mode(0o700).create(&dir) {
                    Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
                    Err(e) => {
                        return Err(e).with_context(|| {
                            format!("Creating a user exclusive tmpdir at {}", dir.display())
                        });
                    }
                    Ok(_) => {}
                }
            }
            dir
        }
        Some(dir) => dir,
    };

    let dir = parent.join(&*SOCKET_DIR_NAME);
    match fs::create_dir(&dir) {
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
        Err(e) => {
            return Err(e).with_context(|| format!("Creating a directory at {}", dir.display()));
        }
        Ok(_) => {}
    }

    Ok(dir)
}

/// Get a sorted list of group ids on POSIX.
///
/// If the client and the server have different lists of groups,
/// then the server should not serve the client.
pub fn groups() -> Option<Vec<u32>> {
    #[cfg(unix)]
    {
        let mut ngroups: libc::c_int = 0;
        if unsafe { libc::getgroups(0, std::ptr::null_mut()) } == -1 {
            ngroups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
            if ngroups <= 0 {
                return None;
            }
        }

        let mut groups: Vec<libc::gid_t> = vec![0; ngroups as usize];
        ngroups = unsafe { libc::getgroups(ngroups, groups.as_mut_ptr()) };
        if ngroups < 0 {
            return None;
        }

        groups.truncate(ngroups as _);
        groups.sort_unstable();
        return Some(groups.into_iter().map(|v| v as u32).collect());
    }

    #[allow(unreachable_code)]
    None
}

/// Get the `RLIMIT_NOFILE` limit on POSIX.
///
/// If the client has a higher limit than the server, then the server
/// should not serve the client.
pub fn rlimit_nofile() -> Option<u64> {
    #[cfg(unix)]
    {
        let mut rlim: libc::rlimit = unsafe { std::mem::zeroed() };
        if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut rlim) } == 0 {
            return Some(rlim.rlim_cur as _);
        }
    }

    None
}

/// Get the umask on POSIX.
pub fn get_umask() -> Option<u32> {
    #[cfg(unix)]
    unsafe {
        let mask = libc::umask(0);
        libc::umask(mask);
        return Some(mask as _);
    }
    #[allow(unreachable_code)]
    None
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::path::Path;
use std::process::Command;
use std::process::Stdio;
use std::time::Instant;

use anyhow::Result;
use log::error;
use log::info;

use crate::error::*;

pub struct CloudSyncTrigger;

impl CloudSyncTrigger {
    pub fn fire<P: AsRef<Path>>(
        sid: &String,
        path: P,
        retries: u32,
        version: Option<u64>,
        workspace: String,
        reason: String,
    ) -> Result<()> {
        let mut workspace_args = vec!["--raw-workspace-name".to_owned(), workspace];
        if let Some(version) = version {
            workspace_args.append(&mut vec![
                "--workspace-version".to_owned(),
                version.to_string(),
            ]);
        }
        for i in 0..retries {
            let now = Instant::now();
            let child = Command::new("hg")
                .current_dir(&path)
                .env(identity::default().env_name("PLAIN").as_ref(), "hint")
                .args(vec!["cloud", "sync"])
                .arg("--check-autosync-enabled")
                .arg("--use-bgssh")
                .arg("--best-effort")
                .args(&workspace_args)
                .args(&vec!["--reason", &reason])
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()?; // do not retry if failed to start

            info!(
                "{} Fire `hg cloud sync` attempt {}, spawned process id '{}'",
                sid,
                i,
                child.id()
            );

            let output = child.wait_with_output()?;

            info!(
                "{} stdout: \n{}",
                sid,
                String::from_utf8_lossy(&output.stdout).trim()
            );
            info!(
                "{} stderr: \n{}",
                sid,
                String::from_utf8_lossy(&output.stderr).trim()
            );
            let end = now.elapsed();
            info!(
                "{} Cloud Sync time: {} sec {} ms",
                sid,
                end.as_secs(),
                end.subsec_nanos() as u64 / 1_000_000
            );
            if !output.status.success() {
                error!("{} Process exited with: {}", sid, output.status);
                if i == retries - 1 {
                    return Err(ErrorKind::CommitCloudHgCloudSyncError(format!(
                        "process exited with: {}, retry later",
                        output.status
                    ))
                    .into());
                }
            } else {
                info!("{} Cloud Sync was successful", sid);
                return Ok(());
            }
        }
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::path::PathBuf;

use serde::Deserialize;

mod defaults {
    use std::path::PathBuf;

    /// Important! Have to be in sync with commitcloudutil.py
    /// Unix:
    ///    returns the value of the 'HOME' environment variable
    ///    if it is set and not equal to the empty string
    /// Windows:
    ///    returns the value of the 'APPDATA' environment variable
    ///    if it is set and not equal to the empty string

    fn home_dir_os() -> Option<PathBuf> {
        let var = {
            #[cfg(not(target_os = "windows"))]
            {
                "HOME"
            }
            #[cfg(target_os = "windows")]
            {
                "APPDATA"
            }
        };
        match ::std::env::var_os(var) {
            Some(ref val) if !val.is_empty() => Some(val),
            _ => None,
        }
        .map(|value| PathBuf::from(value))
    }

    pub fn connected_subscribers_path() -> Option<PathBuf> {
        home_dir_os()
    }

    pub fn user_token_path() -> Option<PathBuf> {
        #[cfg(target_os = "macos")]
        {
            None
        }
        #[cfg(not(target_os = "macos"))]
        {
            home_dir_os()
        }
    }

    pub fn cloudsync_retries() -> u32 {
        2
    }

    pub fn tcp_receiver_port() -> u16 {
        15432
    }
}

/// Struct for decoding Commit Cloud configuration from TOML.
/// Each field has default implementation, meaning that it doesn't have to be present in TOML.

#[derive(Debug, Deserialize)]
pub struct CommitCloudConfig {
    /// Server-Sent Events endpoint for real-time Commit Cloud Notifications
    #[serde(default)]
    pub notification_url: Option<String>,

    /// Path to the directory containing current connected subscribers
    /// This is an optional override, see logic for the default location
    /// Subscriber is a simple ini file containing repo_name, repo_root and workspace
    /// Filename for a subscriber can be any, just make it unique
    /// Mercurial is responsible for adding/removing subscribers into this folder when necessary
    /// This should be in sync with `hg cloud join` and `hg cloud leave` commands
    /// Have to be in sync with 'connected_subscribers_path' option in mercurial config if defined
    #[serde(default = "defaults::connected_subscribers_path")]
    pub connected_subscribers_path: Option<PathBuf>,

    /// Path to the directory containing .commitcloudrc file with OAuth token
    /// that is valid for Server-Sent Events Commit Cloud endpoint and Http endpoint
    /// This is an optional override, see logic for the default location
    /// Have to be in sync with 'user_token_path' option in mercurial config if defined
    /// Macos default storage to token is keychain
    #[serde(default = "defaults::user_token_path")]
    pub user_token_path: Option<PathBuf>,

    /// Number of retries when we trigger `hg cloud sync`
    #[serde(default = "defaults::cloudsync_retries")]
    pub cloudsync_retries: u32,

    /// Tcp port to run a receiver
    /// This is a simple receiver working on tcp socket
    #[serde(default = "defaults::tcp_receiver_port")]
    pub tcp_receiver_port: u16,
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use lazy_static::lazy_static;
use mime::Mime;
use regex::Regex;
use reqwest::StatusCode;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ErrorKind {
    #[error("Commit Cloud `hg cloud sync` error: {0}")]
    CommitCloudHgCloudSyncError(String),
    #[error("Commit Cloud config error: {0}")]
    CommitCloudConfigError(&'static str),
    #[error("Commit Cloud EventSource HTTP error: {}", RE.replace_all(.0, ""))] // remove any token
    CommitCloudHttpError(String),
    #[error("Unexpected error: {0}")]
    CommitCloudUnexpectedError(String),
    #[error("EventSource: HTTP status code: {0}")]
    EventSourceHttp(StatusCode),
    #[error("EventSource: unexpected Content-Type: {0}")]
    EventSourceInvalidContentType(Mime),
    #[error("EventSource: Content-Type missing")]
    EventSourceNoContentType(),
}

lazy_static! {
    static ref RE: Regex = Regex::new(r"[\&\?]?access_token=\b\w+\b").unwrap();
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

pub(crate) mod action;
pub mod config;
pub mod error;
pub mod receiver;
pub mod subscriber;
pub(crate) mod util;

pub use config::CommitCloudConfig;
pub use receiver::TcpReceiverService as CommitCloudTcpReceiverService;
pub use subscriber::WorkspaceSubscriberService as CommitCloudWorkspaceSubscriberService;

pub(crate) type ActionsMap =
    std::collections::HashMap<receiver::CommandName, Box<dyn Fn() + Send + Sync>>;

#[cfg(test)]
pub mod tests;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::marker::Unpin;
use std::net::SocketAddr;
use std::sync::Arc;

use anyhow::Result;
use log::error;
use log::info;
use serde::Deserialize;
use serde::Serialize;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;

use crate::ActionsMap;

/// Set of supported commands
/// All unknown commands will be ignored
#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum CommandName {
    #[serde(rename = "commitcloud::restart_subscriptions")]
    CommitCloudRestartSubscriptions,
    #[serde(rename = "commitcloud::cancel_subscriptions")]
    CommitCloudCancelSubscriptions,
    #[serde(rename = "commitcloud::start_subscriptions")]
    CommitCloudStartSubscriptions,
}

#[derive(Debug, Deserialize, Default, Serialize)]
pub struct CommandData {
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Command(pub (CommandName, CommandData));

/// Simple cross platform commands receiver working on top of Tcp Socket and json
/// Expected commands are in json format
/// Example: ["commitcloud::restart_subscriptions", {"foo": "bar"}]
/// Example to test: echo '["commitcloud::restart_subscriptions", {}]' | nc localhost 15432
/// with_actions builder is used to configure callbacks
/// The serve function starts the service

pub struct TcpReceiverService {
    port: u16,
    actions: Arc<ActionsMap>,
}

impl TcpReceiverService {
    pub fn new(port: u16, actions: ActionsMap) -> TcpReceiverService {
        TcpReceiverService {
            port,
            actions: Arc::new(actions),
        }
    }

    async fn handler(actions: Arc<ActionsMap>, mut socket: impl AsyncRead + Unpin) -> Result<()> {
        let mut buf = Vec::new();
        let bytes_read = socket.read_to_end(&mut buf).await?;

        if bytes_read == 0 {
            // Ping connection, client checks if scm daemon is alive
            // TODO: implement proper health_check request
            return Ok(());
        }

        let command: Command = serde_json::from_slice(&buf[..bytes_read])?;
        let command_name = serde_json::to_string(&(command.0).0)
            .ok()
            .unwrap_or("unknown".into());

        info!("Received {} command", command_name);

        match actions.get(&((command.0).0)) {
            Some(action) => action(),
            None => info!("No actions found for {}", command_name),
        }

        Ok(())
    }

    pub fn serve(self) -> JoinHandle<Result<()>> {
        tokio::spawn(async move {
            info!("Starting CommitCloud TcpReceiverService");
            let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], self.port))).await?;
            info!("Listening on port {}", self.port);
            loop {
                match listener.accept().await {
                    Ok((socket, _)) => {
                        let actions = self.actions.clone();
                        tokio::spawn(async move {
                            if let Err(err) = Self::handler(actions, socket).await {
                                error!("Failed to handle connection: {err}")
                            }
                        });
                    }
                    Err(err) => error!("{err}"),
                }
            }
        })
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::path::PathBuf;
use std::str;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::Arc;
use std::time::Duration;

use anyhow::bail;
use anyhow::Result;
use futures::stream::StreamExt;
use log::error;
use log::info;
use parking_lot::Mutex;
use reqwest::Url;
use reqwest_eventsource::Event;
use reqwest_eventsource::EventSource;
use serde::Deserialize;

use crate::action::CloudSyncTrigger;
use crate::config::CommitCloudConfig;
use crate::error::*;
use crate::receiver::CommandName;
use crate::receiver::CommandName::CommitCloudCancelSubscriptions;
use crate::receiver::CommandName::CommitCloudRestartSubscriptions;
use crate::receiver::CommandName::CommitCloudStartSubscriptions;
use crate::util;
use crate::ActionsMap;

#[derive(Deserialize)]
pub struct Notification {
    pub(crate) version: u64,
}

#[derive(PartialEq, Eq, Hash)]
pub struct Subscription {
    pub(crate) repo_name: String,
    pub(crate) workspace: String,
}

/// WorkspaceSubscriberService manages a set of running subscriptions
/// and trigger `hg cloud sync` on notifications
/// The workflow is simple:
/// * fire `hg cloud sync` on start in every repo
/// * read and start current set of subscriptions and
///     fire `hg cloud sync` on notifications
/// * fire `hg cloud sync` when connection recovers
/// * also provide actions (callbacks) to a few TcpReceiver commands
///     the commands are:
///         "start_subscriptions"
///         "restart_subscriptions"
///         "cancel_subscriptions"
///     if a command comes, gracefully cancel all previous subscriptions
///     and restart if requested
///     main use case:
///     if a cient (hg) add itself as a new subscriber (hg cloud join),
///     it is also client's responsibility to send "restart_subscriptions" command
///     same for unsubscribing (hg cloud leave)
/// The serve function starts the service

pub struct WorkspaceSubscriberService {
    /// Server-Sent Events endpoint for Commit Cloud Notifications
    pub(crate) notification_url: String,

    /// OAuth token path (optional) for access to Commit Cloud SSE endpoint
    pub(crate) user_token_path: Option<PathBuf>,

    /// Directory with connected subscribers
    pub(crate) connected_subscribers_path: PathBuf,

    /// Number of retries for `hg cloud sync`
    pub(crate) cloudsync_retries: u32,

    /// Channel for communication between threads
    pub(crate) channel: (mpsc::Sender<CommandName>, mpsc::Receiver<CommandName>),

    /// Interrupt barrier for joining threads
    pub(crate) interrupt: Arc<AtomicBool>,
}

impl WorkspaceSubscriberService {
    pub fn new(config: &CommitCloudConfig) -> Result<WorkspaceSubscriberService> {
        Ok(WorkspaceSubscriberService {
            notification_url: config
                .notification_url
                .clone()
                .ok_or_else(|| ErrorKind::CommitCloudConfigError("undefined 'notification_url'"))?,
            user_token_path: config.user_token_path.clone(),
            connected_subscribers_path: config.connected_subscribers_path.clone().ok_or_else(
                || ErrorKind::CommitCloudConfigError("undefined 'connected_subscribers_path'"),
            )?,
            cloudsync_retries: config.cloudsync_retries,
            channel: mpsc::channel(),
            interrupt: Arc::new(AtomicBool::new(false)),
        })
    }

    pub fn actions(&self) -> ActionsMap {
        let mut actions = ActionsMap::new();
        actions.insert(CommitCloudRestartSubscriptions, {
            let sender = Mutex::new(self.channel.0.clone());
            let interrupt = self.interrupt.clone();
            Box::new(
                move || match sender.lock().send(CommitCloudRestartSubscriptions) {
                    Err(err) => error!(
                        "Send CommitCloudRestartSubscriptions via mpsc::channel failed, reason: {}",
                        err
                    ),
                    Ok(_) => {
                        info!("Restart subscriptions can take a while because it is graceful");
                        interrupt.store(true, Ordering::Relaxed);
                    }
                },
            )
        });
        actions.insert(CommitCloudCancelSubscriptions, {
            let sender = Mutex::new(self.channel.0.clone());
            let interrupt = self.interrupt.clone();
            Box::new(
                move || match sender.lock().send(CommitCloudCancelSubscriptions) {
                    Err(err) => error!(
                        "Send CommitCloudCancelSubscriptions via mpsc::channel failed with {}",
                        err
                    ),
                    Ok(_) => {
                        info!("Cancel subscriptions can take a while because it is graceful");
                        interrupt.store(true, Ordering::Relaxed);
                    }
                },
            )
        });
        actions.insert(CommitCloudStartSubscriptions, {
            let sender = Mutex::new(self.channel.0.clone());
            let interrupt = self.interrupt.clone();
            Box::new(
                move || match sender.lock().send(CommitCloudStartSubscriptions) {
                    Err(err) => error!(
                        "Send CommitCloudStartSubscriptions via mpsc::channel failed with {}",
                        err
                    ),
                    Ok(_) => {
                        info!("Starting subscriptions.");
                        interrupt.store(true, Ordering::Relaxed);
                    }
                },
            )
        });
        actions
    }

    pub fn serve(self) -> Result<tokio::task::JoinHandle<Result<()>>> {
        self.channel.0.send(CommitCloudStartSubscriptions)?;
        Ok(tokio::spawn(async move {
            info!("Starting CommitCloud Workspace Subscriber Service");
            loop {
                let command = self.channel.1.recv_timeout(Duration::from_secs(60));
                match command {
                    Ok(CommitCloudCancelSubscriptions) => {
                        info!(
                            "All previous subscriptions have been canceled! \
                             Waiting for another commands..."
                        );
                        self.interrupt.store(false, Ordering::Relaxed);
                    }
                    Ok(CommitCloudRestartSubscriptions) => {
                        info!(
                            "All previous subscriptions have been canceled! \
                             Restarting subscriptions..."
                        );
                        self.interrupt.store(false, Ordering::Relaxed);
                        // start subscription threads
                        let access_token = util::read_access_token(&self.user_token_path);
                        if let Ok(access_token) = access_token {
                            let subscriptions = self.run_subscriptions(access_token)?;
                            for child in subscriptions {
                                let _ = child.await;
                            }
                        } else {
                            info!("User is not authenticated with Commit Cloud yet");
                            continue;
                        }
                    }
                    Ok(CommitCloudStartSubscriptions) => {
                        info!("Starting subscriptions...");
                        self.interrupt.store(false, Ordering::Relaxed);
                        let access_token = util::read_access_token(&self.user_token_path);
                        // start subscription threads
                        if let Ok(access_token) = access_token {
                            let subscriptions = self.run_subscriptions(access_token)?;
                            for child in subscriptions {
                                let _ = child.await;
                            }
                        } else {
                            info!("User is not authenticated with Commit Cloud yet");
                            continue;
                        }
                    }
                    Err(mpsc::RecvTimeoutError::Timeout) => {
                        if !util::read_subscriptions(&self.connected_subscribers_path)?.is_empty() {
                            self.channel.0.send(CommitCloudStartSubscriptions)?;
                        }
                        continue;
                    }
                    Err(e) => {
                        error!("Receive from mpsc::channel failed with {}", e);
                        bail!("Receive and wait on mpsc::channel failed with {}", e);
                    }
                }
            }
        }))
    }

    /// This helper function reads the list of current connected subscribers
    /// It starts all the requested subscriptions by creating a separate async task for each one
    /// All tasks keep checking the interrupt flag and join gracefully if it is restart or stop

    fn run_subscriptions(
        &self,
        access_token: util::Token,
    ) -> Result<Vec<tokio::task::JoinHandle<()>>> {
        util::read_subscriptions(&self.connected_subscribers_path)?
            .into_iter()
            .map(|(subscription, repo_roots)| {
                self.run_subscription(access_token.clone(), subscription, repo_roots)
            })
            .collect::<Result<Vec<tokio::task::JoinHandle<()>>>>()
    }

    /// Helper function to run a single subscription

    fn run_subscription(
        &self,
        access_token: util::Token,
        subscription: Subscription,
        repo_roots: Vec<PathBuf>,
    ) -> Result<tokio::task::JoinHandle<()>> {
        let mut notification_url = Url::parse(&self.notification_url)?;

        let sid = format!("({} @ {})", subscription.repo_name, subscription.workspace);
        info!("{} Subscribing to {}", sid, notification_url);

        notification_url
            .query_pairs_mut()
            .append_pair("workspace", &subscription.workspace)
            .append_pair("repo_name", &subscription.repo_name)
            .append_pair("access_token", &access_token.token)
            .append_pair("token_type", &access_token.token_type.to_string());

        let mut es = EventSource::get(notification_url);

        info!("{} Spawn a task to handle the subscription", sid);

        let cloudsync_retries = self.cloudsync_retries;
        let interrupt = self.interrupt.clone();

        Ok(tokio::spawn(async move {
            info!("{} Task started...", sid);

            let fire = |reason: &'static str, version: Option<u64>| {
                for repo_root in repo_roots.iter() {
                    info!(
                        "{} Fire CloudSyncTrigger in '{}' {}",
                        sid,
                        repo_root.display(),
                        reason,
                    );
                    // log outputs, results and continue even if unsuccessful
                    let _res = CloudSyncTrigger::fire(
                        &sid,
                        repo_root,
                        cloudsync_retries,
                        version,
                        subscription.workspace.clone(),
                        format!("scm_daemon: {}", reason),
                    );
                    if interrupt.load(Ordering::Relaxed) {
                        break;
                    }
                }
            };

            fire("before starting subscription", None);
            if interrupt.load(Ordering::Relaxed) {
                return;
            }

            info!("{} Start listening to notifications", sid);

            while !interrupt.load(Ordering::Relaxed) {
                match tokio::time::timeout(Duration::from_millis(500), es.next()).await {
                    Ok(Some(event)) => {
                        let event =
                            event.map_err(|e| ErrorKind::CommitCloudHttpError(format!("{}", e)));

                        match event {
                            Err(e) => {
                                error!("{} Restarting subscriptions due to error: {}...", sid, e);
                                interrupt.store(true, Ordering::Relaxed);
                            }
                            Ok(Event::Open) => {
                                info!("{} EventSource connection open...", sid)
                            }
                            Ok(Event::Message(e)) => {
                                let data = e.data;
                                let notification = serde_json::from_str::<Notification>(&data);
                                if let Err(e) = notification {
                                    error!(
                                        "{} Unable to decode json data in the event, reason: {}. Continue...",
                                        sid, e
                                    );
                                    continue;
                                }
                                let notification = notification.unwrap();
                                info!(
                                    "{} Notification to sync version {} (full message: {})",
                                    sid, notification.version, &data
                                );
                                fire("on new version notification", Some(notification.version));
                            }
                        }
                    }
                    _ => continue,
                }
            }
        }))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use tempfile::tempdir;

use crate::util::read_access_token;
use crate::util::TOKEN_FILENAME;

#[test]
fn test_read_access_token_from_file_should_return_token() {
    let dir = tempdir().unwrap();
    let path = dir.path().join(TOKEN_FILENAME);
    let mut tmp = File::create(path).unwrap();
    writeln!(tmp, "[commitcloud]").unwrap();
    writeln!(tmp, "user_token=token").unwrap();
    let result = read_access_token(&Some(PathBuf::from(dir.path()))).unwrap();
    drop(tmp);
    dir.close().unwrap();
    assert_eq!(result.token, "token");
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str;
use std::time::SystemTime;

use anyhow::bail;
use anyhow::Result;
use filetime::FileTime;
use ini::Ini;
use log::error;
use log::info;

use crate::error::*;
use crate::subscriber::Subscription;

static JOINED_DIR: &str = ".commitcloud";
static JOINED: &str = "joined";
static SEC_IN_WEEK: u64 = 604800;

/// Map from a subscription to list of repo roots
pub fn read_subscriptions(
    joined_pool_path: &PathBuf,
) -> Result<HashMap<Subscription, Vec<PathBuf>>> {
    let mut joined_pool_path = joined_pool_path.clone();
    joined_pool_path.push(JOINED_DIR);
    joined_pool_path.push(JOINED);

    info!(
        "Reading subscription requests from '{}' folder...",
        joined_pool_path.display()
    );

    let paths = fs::read_dir(joined_pool_path);
    if let &Err(ref e) = &paths {
        if e.kind() == io::ErrorKind::NotFound {
            info!("No active subscribers");
            return Ok(HashMap::new());
        }
        error!("{}", e);
    }

    let paths = paths?
        .filter(|result| result.is_ok())
        .map(|dir| dir.unwrap().path());

    let mut subscriptions: HashMap<Subscription, Vec<PathBuf>> = HashMap::new();

    for ref path in paths {
        let metadata = fs::metadata(path)?;
        let mtime = FileTime::from_last_modification_time(&metadata).unix_seconds() as u64;
        let timenow = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)?
            .as_secs();
        if mtime + SEC_IN_WEEK < timenow {
            info!(
                "Removing old subscription file '{}' (mtime ts: {}). The client needs to resubscribe.",
                path.display(),
                mtime
            );
            let res = fs::remove_file(path);
            if res.is_ok() {
                continue;
            } else {
                info!(
                    "Failed to clean up old subscription file '{}'. The subscription remains active.",
                    path.display(),
                );
            }
        }
        if let Ok(ref mut file) = fs::OpenOptions::new().read(true).open(path) {
            let ini = Ini::read_from(&mut io::BufReader::new(file))?;
            let section = ini.section(Some("commitcloud"));
            if let Some(section) = section {
                // strip whitespaces around the fields
                let workspace = section.get("workspace").map(|workspace| workspace.trim());
                let repo_name = section.get("repo_name").map(|repo_name| repo_name.trim());
                let repo_root = section
                    .get("repo_root")
                    .map(|repo_root| PathBuf::from(repo_root.trim()));

                if workspace.is_none() || repo_name.is_none() || repo_root.is_none() {
                    info!(
                        "Skipping the file '{}' because format is invalid",
                        path.display()
                    );
                } else {
                    let workspace = workspace.unwrap();
                    let repo_name = repo_name.unwrap();
                    let repo_root = repo_root.unwrap();

                    if !Path::new(&repo_root).exists() || !Path::new(&repo_root).is_dir() {
                        info!(
                            "Skipping the file '{}' because 'repo_root' '{}' \
                             is not an existing directory",
                            path.display(),
                            repo_root.display()
                        );
                        continue;
                    }
                    let subscription = Subscription {
                        repo_name: repo_name.to_string(),
                        workspace: workspace.to_string(),
                    };
                    {
                        if let Some(entry) = subscriptions.get_mut(&subscription) {
                            (*entry).push(repo_root);
                            continue;
                        }
                    }
                    subscriptions.insert(subscription, vec![repo_root]);
                }
            } else {
                info!(
                    "Skipping the file '{}' because format is invalid",
                    path.display()
                );
            }
        }
    }

    info!(
        "Found {} active subscription{}",
        subscriptions.len(),
        if subscriptions.len() != 1 { "s" } else { "" }
    );

    for (key, value) in &subscriptions {
        info!(
            "Found {} subscription request{} for repo '{}' and workspace '{}'",
            value.len(),
            if value.len() != 1 { "s" } else { "" },
            key.repo_name,
            key.workspace
        );
    }
    return Ok(subscriptions);
}

pub static TOKEN_FILENAME: &str = ".commitcloudrc";

#[derive(Clone, PartialEq, Debug)]
pub enum TokenType {
    OAuth,
    Cat,
}

impl fmt::Display for TokenType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TokenType::OAuth => write!(f, "oauth"),
            TokenType::Cat => write!(f, "cat"),
        }
    }
}

#[derive(Clone)]
pub struct Token {
    pub(crate) token: String,
    pub(crate) token_type: TokenType,
}

pub fn read_access_token(user_token_path: &Option<PathBuf>) -> Result<Token> {
    // Try to read OAuth token from a file.
    let token = if let &Some(ref user_token_path) = user_token_path {
        let mut user_token_path = user_token_path.clone();
        user_token_path.push(TOKEN_FILENAME);
        info!(
            "Token Lookup: reading commitcloud OAuth token from a file {}...",
            user_token_path.display()
        );
        match fs::OpenOptions::new().read(true).open(user_token_path) {
            Ok(ref mut file) => Ini::read_from(&mut io::BufReader::new(file))?
                .get_from(Some("commitcloud"), "user_token")
                .map(|s| s.trim().to_string()),
            Err(ref e) if e.kind() == io::ErrorKind::NotFound => None,
            Err(err) => {
                error!("{}", err);
                bail!(err)
            }
        }
    } else {
        None
    };
    if let Some(token) = token {
        return Ok(Token {
            token,
            token_type: TokenType::OAuth,
        });
    }

    // Try to issue a CAT token automatically.
    let clicat_tool = if hostcaps::is_prod() {
        "clicat"
    } else {
        "corp_clicat"
    };
    info!(
        "Token Lookup: generating commitcloud CAT token via {}...",
        clicat_tool
    );
    let ten_mins_seconds = 600;
    let output = Command::new(clicat_tool)
        .args(vec![
            "create",
            "--verifier_type",
            "SERVICE_IDENTITY",
            "--verifier_id",
            "commitcloud-service",
            "--token_timeout_seconds",
            &ten_mins_seconds.to_string(),
        ])
        .output();

    match output {
        Err(e) => {
            if let io::ErrorKind::NotFound = e.kind() {
                info!("`{}` executable is not found", clicat_tool);
            } else {
                error!("`{}` failed: {}", clicat_tool, e)
            }
        }
        Ok(output) => {
            if !output.status.success() {
                error!(
                    "CAT token: failed to generate via {}, process exited with {}",
                    clicat_tool, output.status
                );
            } else {
                info!("CAT token has been generated");
                return Ok(Token {
                    token: str::from_utf8(&output.stdout)?.trim().to_string(),
                    token_type: TokenType::Cat,
                });
            }
        }
    }

    Err(ErrorKind::CommitCloudUnexpectedError("Token Lookup: token not found".into()).into())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;
use std::fmt;
use std::ops::Range;

/// Instruction from parsed config.
#[derive(Clone, Debug)]
pub enum Instruction<'a> {
    /// Set a config.
    SetConfig {
        section: &'a str,
        name: &'a str,
        // For multi-line value, it can be an owned string.
        value: Cow<'a, str>,
        span: Range<usize>,
    },
    /// Unset a config.
    UnsetConfig {
        section: &'a str,
        name: &'a str,
        span: Range<usize>,
    },
    /// Include another config file.
    Include { path: &'a str, span: Range<usize> },
}

type ParseOutput<'a> = Vec<Instruction<'a>>;

pub fn parse<'a>(text: &'a str) -> Result<ParseOutput<'a>, Error> {
    let ctx = Context { buf: text };
    ctx.parse()
}

struct Context<'a> {
    buf: &'a str,
}

#[derive(Debug)]
pub struct Error {
    line_no: usize,
    message: &'static str,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "line {}: {}", self.line_no + 1, self.message)
    }
}

impl std::error::Error for Error {}

impl<'a> Context<'a> {
    fn parse(&self) -> Result<ParseOutput<'a>, Error> {
        let mut output = Vec::with_capacity(self.instruction_size_hint());

        // Parser state.
        let mut section: &'a str = "";
        let mut name: &'a str = "";
        // For single or multi-line value.
        let mut value_lines: Vec<&'a str> = Vec::with_capacity(1);

        for (line_no, line) in self.buf.lines().enumerate().chain(std::iter::once((0, ""))) {
            let first_char = line.chars().next().unwrap_or('#');
            let value_empty: bool = value_lines.is_empty();
            // Multi-line config.
            if !value_empty && " \t".contains(first_char) {
                value_lines.push(line.trim());
                continue;
            }
            // Push parsed config.
            if !value_empty {
                let span = get_range(
                    self.buf,
                    value_lines.first().unwrap(),
                    value_lines.last().unwrap(),
                );
                let value = if value_lines.len() == 1 {
                    Cow::Borrowed(value_lines[0])
                } else {
                    // Strip empty lines at the end.
                    let mut n = value_lines.len();
                    while n > 0 && value_lines[n - 1].is_empty() {
                        n -= 1;
                    }
                    Cow::Owned(value_lines[..n].join("\n"))
                };
                let inst = Instruction::SetConfig {
                    section,
                    name,
                    value,
                    span,
                };
                output.push(inst);
                value_lines.clear();
            }
            // Handle different lines.
            match first_char {
                // [section] (space)
                '[' => {
                    let rest;
                    (section, rest) = match line[1..].split_once(']') {
                        None => {
                            return Err(Error {
                                line_no,
                                message: "missing ']' for section header",
                            });
                        }
                        Some((section, rest)) => (section.trim(), rest.trim()),
                    };
                    if !rest.is_empty() {
                        return Err(Error {
                            line_no,
                            message: "extra content after section header",
                        });
                    }
                    if section.is_empty() {
                        return Err(Error {
                            line_no,
                            message: "empty section name",
                        });
                    }
                }
                // # comment
                ';' | '#' => {
                    continue;
                }
                // blank line
                ' ' | '\t' => {
                    if line.trim().is_empty() {
                        continue;
                    }
                    return Err(Error {
                        line_no,
                        message: "indented line is not part of a multi-line config",
                    });
                }
                // %include or %unset
                '%' => {
                    if let Some(rest) = line.strip_prefix("%include ") {
                        let path = rest.trim();
                        let span = get_range(self.buf, path, path);
                        let inst = Instruction::Include { path, span };
                        output.push(inst);
                    } else if let Some(rest) = line.strip_prefix("%unset ") {
                        let name = rest.trim();
                        let span = get_range(self.buf, name, name);
                        if name.contains('=') {
                            return Err(Error {
                                line_no,
                                message: "config name cannot include '='",
                            });
                        }
                        let inst = Instruction::UnsetConfig {
                            section,
                            name,
                            span,
                        };
                        output.push(inst);
                    } else {
                        return Err(Error {
                            line_no,
                            message: "unknown directive (expect '%include' or '%unset')",
                        });
                    }
                }
                // name = value
                _ => {
                    let value;
                    (name, value) = match line.split_once('=') {
                        None => {
                            return Err(Error {
                                line_no,
                                message: "expect '[section]' or 'name = value'",
                            });
                        }
                        Some((name, value)) => (name.trim(), value.trim()),
                    };
                    if name.is_empty() {
                        return Err(Error {
                            line_no,
                            message: "empty config name",
                        });
                    }
                    value_lines.push(value);
                }
            }
        }

        Ok(output)
    }

    fn instruction_size_hint(&self) -> usize {
        self.buf
            .lines()
            .filter(|l| l.starts_with('%') || l.contains('='))
            .count()
    }
}

/// Figure out a range in `text` so `text[range]` starts with the first byte
/// of `start` and ends with the last byte of `end`.
/// Assumes `start` and `end` are derived from (sub-strings of) `text`.
fn get_range(text: &str, start: &str, end: &str) -> Range<usize> {
    let text_offset: usize = text.as_ptr() as usize;
    let start_offset: usize = start.as_ptr() as usize;
    let end_offset: usize = (end.as_ptr() as usize) + end.len();
    assert!(end_offset <= text_offset + text.len());
    assert!(start_offset >= text_offset);
    assert!(start_offset <= end_offset);
    (start_offset - text_offset)..(end_offset - text_offset)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # Parsing hgrc content using pest.
//!
//! Parse hgrc content (`str`) into a list of instructions:
//! - SetConfig(section, name, value)
//! - UnsetConfig(section, name)
//! - Include(path)
//!
//! Pure. Do not depend on a filesystem.

pub(crate) mod config;
#[cfg(test)]
mod tests;

pub use config::parse;
pub use config::Instruction;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use crate::parse;

#[test]
fn test_parse_basic() {
    let config = r#"
# comment
[section1]
name1=value1
name2 = value2

[section2]
name3 = mutliple line,
  line 2
  line 3
    line 4

%unset name3
%include bar
"#;
    assert_eq!(
        format!("{:#?}", parse(config).unwrap()),
        r#"[
    SetConfig {
        section: "section1",
        name: "name1",
        value: "value1",
        span: 28..34,
    },
    SetConfig {
        section: "section1",
        name: "name2",
        value: "value2",
        span: 43..49,
    },
    SetConfig {
        section: "section2",
        name: "name3",
        value: "mutliple line,\nline 2\nline 3\nline 4",
        span: 70..113,
    },
    UnsetConfig {
        section: "section2",
        name: "name3",
        span: 122..127,
    },
    Include {
        path: "bar",
        span: 137..140,
    },
]"#
    );
}

#[test]
fn test_parse_error() {
    let config = "%set a b";
    assert_eq!(
        format!("{}", parse(config).unwrap_err()),
        "line 1: unknown directive (expect '%include' or '%unset')"
    );
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::fs;
use std::io;
use std::path::Path;

fn check_signed_source() -> io::Result<()> {
    // Check if lib.rs is in sync with the Thrift compiler.
    let manifest_dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    let crate_path = Path::new(&manifest_dir_path);
    let fbcode_path = crate_path.ancestors().nth(4).unwrap();

    let thrift_file_path =
        fbcode_path.join("configerator/structs/scm/hg/hgclientconf/hgclient.thrift");
    if !thrift_file_path.exists() {
        // Do not make it a fatal error on non-fbcode environment (ex. OSS).
        println!(
            "cargo:warning=Does not verify Thrift file at {}.",
            &thrift_file_path.display()
        );
        return Ok(());
    }
    println!(
        "cargo:rerun-if-changed={}",
        &thrift_file_path.to_string_lossy()
    );

    let thrift_file_content = fs::read_to_string(&thrift_file_path)?;

    let hash = "4bc06e1c39884f65a4e9cd145972df39";
    if !thrift_file_content.contains(hash) {
        let msg = format!(
            "thrift_types.rs and HASH need update: {} hash mismatch (expect: {})",
            &thrift_file_path.display(),
            hash,
        );
        return Err(io::Error::new(io::ErrorKind::Other, msg));
    }

    Ok(())
}

fn main() -> io::Result<()> {
    check_signed_source()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use configloader::config::ConfigSet;
use configloader::hg::ConfigSetHgExt;
use configloader::Text;
use minibench::bench;
use minibench::elapsed;

fn main() {
    bench("parse 645KB file", || {
        let mut config_file = String::new();
        for _ in 0..100 {
            for section in b'a'..b'z' {
                config_file += &format!("[{ch}{ch}{ch}{ch}]\n", ch = section as char);
                for name in b'a'..b'z' {
                    config_file += &format!("{ch}{ch}{ch} = {ch}{ch}{ch}\n", ch = name as char);
                }
            }
        }
        let text = Text::from(config_file);
        elapsed(|| {
            let mut cfg = ConfigSet::new();
            cfg.parse(text.clone(), &"bench".into());
        })
    });

    bench("load system and user", || {
        elapsed(|| {
            let mut cfg = ConfigSet::new();
            cfg.load::<String, String>(None, None).unwrap();
        })
    });

    bench("load repo", || {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path();
        std::fs::create_dir(path.join(".hg")).unwrap();
        std::fs::create_dir(path.join(".sl")).unwrap();
        elapsed(|| {
            let mut cfg = ConfigSet::new();
            cfg.load::<String, String>(Some(path), None).unwrap();
        })
    });
}