/*
 * 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 anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use bookmarks::BookmarkKey;
use context::CoreContext;
use mononoke_types::BonsaiChangeset;
use regex::Regex;

use crate::ChangesetHook;
use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::HookConfig;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct LimitCommitsizeBuilder {
    commit_size_limit: Option<u64>,
    override_limit_path_regexes: Option<Vec<String>>,
    override_limits: Option<Vec<u64>>,
    ignore_path_regexes: Option<Vec<String>>,
    changed_files_limit: Option<u64>,
}

impl LimitCommitsizeBuilder {
    pub fn set_from_config(mut self, config: &HookConfig) -> Self {
        // Please note that the _i64 configs override any i32s one with the same key.
        if let Some(v) = config.ints.get("commitsizelimit") {
            self = self.commit_size_limit(*v as u64)
        }
        if let Some(v) = config.ints_64.get("commitsizelimit") {
            self = self.commit_size_limit(*v as u64)
        }
        if let Some(v) = config.string_lists.get("ignore_path_regexes") {
            self = self.ignore_path_regexes(v)
        }
        if let Some(v) = config.ints.get("changed_files_limit") {
            self = self.changed_files_limit(*v as u64)
        }
        if let Some(v) = config.ints_64.get("changed_files_limit") {
            self = self.changed_files_limit(*v as u64)
        }
        if let Some(v) = config.string_lists.get("override_limit_path_regexes") {
            self = self.override_limit_path_regexes(v);
        }
        if let Some(v) = config.int_lists.get("override_limits") {
            self = self.override_limits(v.iter().map(|i| *i as u64));
        }
        if let Some(v) = config.int_64_lists.get("override_limits") {
            self = self.override_limits(v.iter().map(|i| *i as u64));
        }
        self
    }

    pub fn commit_size_limit(mut self, limit: u64) -> Self {
        self.commit_size_limit = Some(limit);
        self
    }

    pub fn override_limit_path_regexes(
        mut self,
        strs: impl IntoIterator<Item = impl AsRef<str>>,
    ) -> Self {
        self.override_limit_path_regexes =
            Some(strs.into_iter().map(|s| String::from(s.as_ref())).collect());
        self
    }

    pub fn override_limits(mut self, limits: impl IntoIterator<Item = u64>) -> Self {
        self.override_limits = Some(limits.into_iter().collect());
        self
    }

    pub fn ignore_path_regexes(mut self, strs: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
        self.ignore_path_regexes =
            Some(strs.into_iter().map(|s| String::from(s.as_ref())).collect());
        self
    }

    pub fn changed_files_limit(mut self, changed_files_limit: u64) -> Self {
        self.changed_files_limit = Some(changed_files_limit);
        self
    }

    pub fn build(self) -> Result<LimitCommitsize> {
        let regexes = self
            .override_limit_path_regexes
            .unwrap_or_default()
            .into_iter()
            .map(|s| Regex::new(&s))
            .collect::<Result<Vec<_>, _>>()
            .context("Failed to create regex for override_limit_path_regexes")?;

        let limits = self
            .override_limits
            .unwrap_or_default()
            .into_iter()
            .collect::<Vec<_>>();

        if regexes.len() != limits.len() {
            return Err(anyhow!(
                "Failed to initialize limit_commitsize hook. Lists 'override_limit_path_regexes' and 'override_limits' have different sizes."
            ));
        }

        let regexes_with_limits: Vec<(Regex, u64)> =
            regexes.into_iter().zip(limits.into_iter()).collect();

        Ok(LimitCommitsize {
            commit_size_limit: self
                .commit_size_limit
                .ok_or_else(|| anyhow!("Missing commitsizelimit config"))?,
            override_limit_path_regexes_with_limits: regexes_with_limits,
            ignore_path_regexes: self
                .ignore_path_regexes
                .unwrap_or_default()
                .into_iter()
                .map(|s| Regex::new(&s))
                .collect::<Result<Vec<_>, _>>()
                .context("Failed to create regex for ignore_path_regex")?,
            changed_files_limit: self.changed_files_limit,
        })
    }
}

pub struct LimitCommitsize {
    commit_size_limit: u64,
    ignore_path_regexes: Vec<Regex>,
    override_limit_path_regexes_with_limits: Vec<(Regex, u64)>,
    changed_files_limit: Option<u64>,
}

impl LimitCommitsize {
    pub fn builder() -> LimitCommitsizeBuilder {
        LimitCommitsizeBuilder::default()
    }
}

#[async_trait]
impl ChangesetHook for LimitCommitsize {
    async fn run<'this: 'cs, 'ctx: 'this, 'cs, 'fetcher: 'cs>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _bookmark: &BookmarkKey,
        changeset: &'cs BonsaiChangeset,
        _content_manager: &'fetcher dyn FileContentManager,
        cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if cross_repo_push_source == CrossRepoPushSource::PushRedirected {
            // For push-redirected commits, we rely on running source-repo hooks
            return Ok(HookExecution::Accepted);
        }

        // find max commit size based on the files in the changeset
        let mut max_commit_size_limit = self.commit_size_limit;
        for (path, _) in changeset.file_changes() {
            let path = format!("{}", path);
            let path_size_limit = self
                .override_limit_path_regexes_with_limits
                .iter()
                .filter(|(regex, _)| regex.is_match(&path))
                .map(|(_, size)| size)
                .max();
            if let Some(limit) = path_size_limit {
                max_commit_size_limit = u64::max(max_commit_size_limit, *limit);
            }
        }

        let mut num_changed_files = 0;
        let mut totalsize = 0;
        for (path, file_change) in changeset.file_changes() {
            let path = format!("{}", path);
            if self
                .ignore_path_regexes
                .iter()
                .any(|regex| regex.is_match(&path))
            {
                continue;
            }

            num_changed_files += 1;
            totalsize += file_change.size().unwrap_or(0);
        }

        if let Some(changed_files_limit) = self.changed_files_limit {
            if num_changed_files > changed_files_limit {
                return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                    "Commit too large",
                    format!(
                        "Commit changed {} files but at most {} are allowed. See https://fburl.com/landing_big_diffs for instructions.",
                        num_changed_files, changed_files_limit,
                    ),
                )));
            }
        }

        if totalsize > max_commit_size_limit {
            return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                "Commit too large",
                format!(
                    "Commit size limit is {} bytes.\n\
                     You tried to push a commit {} bytes in size that is over the limit.\n\
                     See https://fburl.com/landing_big_diffs for instructions.",
                    max_commit_size_limit, totalsize
                ),
            )));
        }

        Ok(HookExecution::Accepted)
    }
}

#[cfg(test)]
mod test {
    use std::collections::HashMap;

    use anyhow::Error;
    use blobstore::Loadable;
    use borrowed::borrowed;
    use fbinit::FacebookInit;
    use hooks_content_stores::RepoFileContentManager;
    use maplit::hashmap;
    use tests_utils::BasicTestRepo;
    use tests_utils::CreateCommitContext;

    use super::*;

    #[fbinit::test]
    async fn test_limitcommitsize(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;
        borrowed!(ctx, repo);

        let cs_id = CreateCommitContext::new_root(ctx, repo)
            .add_file("dir/a", "a")
            .add_file("dir/b", "b")
            .add_file("dir/c", "c")
            .commit()
            .await?;

        let bcs = cs_id.load(ctx, &repo.repo_blobstore).await?;

        let content_manager = RepoFileContentManager::new(&repo);
        let hook = build_hook(hashmap! {
            "commitsizelimit".to_string() => 3,
            "changed_files_limit".to_string() => 3,
        })?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        assert_eq!(hook_execution, HookExecution::Accepted);

        let hook = build_hook(hashmap! {
            "commitsizelimit".to_string() => 3,
            "changed_files_limit".to_string() => 1,
        })?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        match hook_execution {
            HookExecution::Rejected(info) => {
                assert!(info.long_description.contains("changed 3 files"));
            }
            HookExecution::Accepted => {
                return Err(anyhow!("should be rejected"));
            }
        };

        let hook = build_hook(hashmap! {
            "commitsizelimit".to_string() => 1,
            "changed_files_limit".to_string() => 3,
        })?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        match hook_execution {
            HookExecution::Rejected(info) => {
                assert!(info.long_description.contains("commit 3 bytes"));
            }
            HookExecution::Accepted => {
                return Err(anyhow!("should be rejected"));
            }
        };
        Ok(())
    }

    #[fbinit::test]
    async fn test_limitcommitsize_removed_files(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;
        borrowed!(ctx, repo);

        let parent_cs_id = CreateCommitContext::new_root(ctx, repo)
            .add_file("dir/a", "a")
            .add_file("dir/b", "b")
            .commit()
            .await?;

        let cs_id = CreateCommitContext::new(ctx, repo, vec![parent_cs_id])
            .delete_file("dir/a")
            .delete_file("dir/b")
            .commit()
            .await?;

        let bcs = cs_id.load(ctx, &repo.repo_blobstore).await?;

        let content_manager = RepoFileContentManager::new(&repo);
        let hook = build_hook(hashmap! {
            "commitsizelimit".to_string() => 100,
            "changed_files_limit".to_string() => 2,
        })?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        assert_eq!(hook_execution, HookExecution::Accepted);

        let hook = build_hook(hashmap! {
            "commitsizelimit".to_string() => 100,
            "changed_files_limit".to_string() => 1,
        })?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        match hook_execution {
            HookExecution::Rejected(_) => {}
            HookExecution::Accepted => {
                return Err(anyhow!("should be rejected"));
            }
        };
        Ok(())
    }

    #[fbinit::test]
    async fn test_limitcommitsize_override(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;
        borrowed!(ctx, repo);

        let cs_id = CreateCommitContext::new_root(ctx, repo)
            .add_file("dir/a", "a")
            .add_file("dir/b", "b")
            .add_file("odir/c", "c")
            .commit()
            .await?;

        let bcs = cs_id.load(ctx, &repo.repo_blobstore).await?;

        let content_manager = RepoFileContentManager::new(&repo);
        let hook = build_hook_with_limits(
            hashmap! {
                "commitsizelimit".to_string() => 1,
                "changed_files_limit".to_string() => 3,
            },
            hashmap! {
                "override_limit_path_regexes".to_string() => vec!["^dir/.*$".to_string()],
            },
            hashmap! {
                "override_limits".to_string() => vec![3],
            },
        )?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        // override max size is 3 bytes which is enough for 3 bytes commit
        assert_eq!(hook_execution, HookExecution::Accepted);

        Ok(())
    }

    #[fbinit::test]
    async fn test_limitcommitsize_override_hits_limit(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;
        borrowed!(ctx, repo);

        let cs_id = CreateCommitContext::new_root(ctx, repo)
            .add_file("dir/a", "a")
            .add_file("dir/b", "b")
            .add_file("odir/c", "c")
            .commit()
            .await?;

        let bcs = cs_id.load(ctx, &repo.repo_blobstore).await?;

        let content_manager = RepoFileContentManager::new(&repo);

        let hook = build_hook_with_limits(
            hashmap! {
                "commitsizelimit".to_string() => 1,
                "changed_files_limit".to_string() => 3,
            },
            hashmap! {
                "override_limit_path_regexes".to_string() => vec!["^odir/.*$".to_string()],
            },
            hashmap! {
                "override_limits".to_string() => vec![2],
            },
        )?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;
        // override max size is 2 bytes, but commit has 3 in total
        match hook_execution {
            HookExecution::Rejected(_) => {}
            HookExecution::Accepted => {
                return Err(anyhow!("should be rejected"));
            }
        };

        Ok(())
    }

    #[fbinit::test]
    async fn test_limitcommitsize_ignored_files(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BasicTestRepo = test_repo_factory::build_empty(ctx.fb).await?;
        borrowed!(ctx, repo);

        let cs_id = CreateCommitContext::new_root(ctx, repo)
            .add_file("dir/a", "a")
            .add_file("dir/b", "b")
            .add_file("ignored_dir/project/c", "c")
            .add_file("something/ignored_dir/project/d", "d")
            .add_file("test_ignored_dir/schemas/e", "e")
            .add_file("ignored_dir/schemas/f", "f")
            .commit()
            .await?;

        let bcs = cs_id.load(ctx, &repo.repo_blobstore).await?;

        let content_manager = RepoFileContentManager::new(&repo);
        let hook = build_hook_with_limits(
            hashmap! {
                "commitsizelimit".to_string() => 2,
                "changed_files_limit".to_string() => 2,
            },
            hashmap! {
                "ignore_path_regexes".to_string() => vec![r"(^|/)(test_)?ignored_dir/(project|schemas)".to_string()],
            },
            hashmap! {},
        )?;
        let hook_execution = hook
            .run(
                ctx,
                &BookmarkKey::new("book")?,
                &bcs,
                &content_manager,
                CrossRepoPushSource::NativeToThisRepo,
                PushAuthoredBy::User,
            )
            .await?;

        match hook_execution {
            HookExecution::Rejected(_) => {
                return Err(anyhow!(
                    "files in ignored_dir should not count towards limit"
                ));
            }
            HookExecution::Accepted => {}
        };

        Ok(())
    }

    fn build_hook(ints_64: HashMap<String, i64>) -> Result<LimitCommitsize> {
        build_hook_with_limits(ints_64, hashmap! {}, hashmap! {})
    }

    fn build_hook_with_limits(
        ints_64: HashMap<String, i64>,
        string_lists: HashMap<String, Vec<String>>,
        int_lists: HashMap<String, Vec<i32>>,
    ) -> Result<LimitCommitsize> {
        let config = HookConfig {
            bypass: None,
            strings: hashmap! {},
            ints_64,
            string_lists,
            int_lists,
            ..Default::default()
        };
        let mut builder = LimitCommitsize::builder();
        builder = builder.set_from_config(&config);
        builder.build()
    }
}


/*
 * 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 anyhow::Context;
use anyhow::Error;
use async_trait::async_trait;
use bookmarks::BookmarkKey;
use context::CoreContext;
use mononoke_types::BonsaiChangeset;

use crate::ChangesetHook;
use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::HookConfig;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

const DEFAULT_TITLE_LENGTH: usize = 80;

#[derive(Clone, Debug)]
pub struct LimitCommitMessageLength {
    display_title_length: usize,
    length_limit: usize,
}

impl LimitCommitMessageLength {
    pub fn new(config: &HookConfig) -> Result<Self, Error> {
        let display_title_length = config
            .strings
            .get("display_title_length")
            .map(|l| l.parse().context("While parsing display_title_length"))
            .transpose()?
            .unwrap_or(DEFAULT_TITLE_LENGTH);

        let length_limit = config
            .strings
            .get("length_limit")
            .ok_or_else(|| Error::msg("Required config max_length is missing"))?;

        let length_limit = length_limit.parse().context("While parsing length_limit")?;

        Ok(Self {
            display_title_length,
            length_limit,
        })
    }
}

#[async_trait]
impl ChangesetHook for LimitCommitMessageLength {
    async fn run<'this: 'cs, 'ctx: 'this, 'cs, 'fetcher: 'cs>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _bookmark: &BookmarkKey,
        changeset: &'cs BonsaiChangeset,
        _content_manager: &'fetcher dyn FileContentManager,
        _cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution, Error> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        let message = changeset.message();
        let len = message.len();

        let execution = if len >= self.length_limit {
            // Try to find a title to show.
            let title = extract_title(message, self.display_title_length);

            HookExecution::Rejected(HookRejectionInfo::new_long(
                "Commit message too long",
                format!(
                    "Commit message length for '{}' ({}) exceeds length limit (>= {})",
                    title, len, self.length_limit
                ),
            ))
        } else {
            HookExecution::Accepted
        };

        Ok(execution)
    }
}

fn extract_title<'a>(message: &'a str, max_length: usize) -> &'a str {
    let message = if message.len() > max_length {
        &message[0..max_length]
    } else {
        message
    };

    message.split('\n').next().unwrap_or("")
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_extract_title_short() {
        assert_eq!(extract_title("foo\nbar", 4), "foo");
        assert_eq!(extract_title("foo\nbar", 5), "foo");
    }

    #[test]
    fn test_extract_title_long() {
        assert_eq!(extract_title("foo\nbar", 2), "fo");
        assert_eq!(extract_title("foo\nbar", 3), "foo");
    }

    #[test]
    fn test_extract_title_exact() {
        assert_eq!(extract_title("foo", 3), "foo");
    }

    #[test]
    fn test_extract_title_empty() {
        assert_eq!(extract_title("", 3), "");
    }
}


/*
 * 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 anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;
use regex::Regex;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookConfig;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct LimitFilesizeBuilder {
    path_regexes: Option<Vec<String>>,
    limits: Option<Vec<i32>>,
}

impl LimitFilesizeBuilder {
    pub fn set_from_config(mut self, config: &HookConfig) -> Self {
        if let Some(v) = config.string_lists.get("filesize_limits_regexes") {
            self = self.filesize_limits_regexes(v)
        }

        if let Some(v) = config.int_lists.get("filesize_limits_values") {
            self.limits = Some(v.clone())
        }

        self
    }

    pub fn filesize_limits_regexes(
        mut self,
        strs: impl IntoIterator<Item = impl AsRef<str>>,
    ) -> Self {
        self.path_regexes = Some(strs.into_iter().map(|s| String::from(s.as_ref())).collect());
        self
    }

    pub fn build(self) -> Result<LimitFilesize> {
        if let (Some(regexes_str), Some(limits)) = (self.path_regexes, self.limits) {
            if regexes_str.is_empty() || limits.is_empty() {
                return Err(anyhow!(
                    "Failed to initialize limit_filesize hook. Either 'filesize_limits_regexes' or 'filesize_limits_values' list is empty."
                ));
            }
            let regexes = regexes_str
                .into_iter()
                .map(|s| Regex::new(&s))
                .collect::<Result<Vec<_>, _>>()
                .context("Failed to create regex for path_regexes")?;

            let limits: Vec<Option<u64>> = limits.into_iter().map(|n| n.try_into().ok()).collect();

            return Ok(LimitFilesize {
                path_regexes_with_limits: regexes.into_iter().zip(limits.into_iter()).collect(),
            });
        }
        Err(anyhow!(
            "Failed to initialize limit_filesize hook. Either 'filesize_limits_regexes' or 'filesize_limits_values' option is missing."
        ))
    }
}

pub struct LimitFilesize {
    path_regexes_with_limits: Vec<(Regex, Option<u64>)>,
}

impl LimitFilesize {
    pub fn builder() -> LimitFilesizeBuilder {
        LimitFilesizeBuilder::default()
    }
}

#[async_trait]
impl FileHook for LimitFilesize {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        ctx: &'ctx CoreContext,
        content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if cross_repo_push_source == CrossRepoPushSource::PushRedirected {
            // For push-redirected commits, we rely on running source-repo hooks
            return Ok(HookExecution::Accepted);
        }

        let path = format!("{}", path);
        let change = match change {
            Some(c) => c,
            None => return Ok(HookExecution::Accepted),
        };

        let len = content_manager
            .get_file_size(ctx, change.content_id())
            .await?;
        for (regex, maybe_limit) in &self.path_regexes_with_limits {
            if !regex.is_match(&path) {
                continue;
            }
            match maybe_limit {
                None => return Ok(HookExecution::Accepted),
                Some(limit) if len <= *limit => return Ok(HookExecution::Accepted),
                Some(limit) => {
                    return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                        "File too large",
                        format!(
                            "File size limit is {} bytes. You tried to push file {} that is over the limit ({} bytes). This limit is enforced for files matching the following regex: \"{}\". See https://fburl.com/landing_big_diffs for instructions.",
                            limit, path, len, regex
                        ),
                    )));
                }
            }
        }
        Ok(HookExecution::Accepted)
    }
}


/*
 * 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 anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use async_trait::async_trait;
use context::CoreContext;
use mercurial_types::simple_fsencode;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookConfig;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

// The filesystem max is 255.
const MAX_PATH_COMPONENT_LIMIT: usize = 255;

#[derive(Clone, Debug)]
pub struct LimitPathLengthHook {
    length_limit: usize,
}

impl LimitPathLengthHook {
    pub fn new(config: &HookConfig) -> Result<Self, Error> {
        let length_limit = config
            .strings
            .get("length_limit")
            .ok_or_else(|| Error::msg("Required config length_limit is missing"))?;

        let length_limit = length_limit.parse().context("While parsing length_limit")?;

        Ok(Self { length_limit })
    }
}

#[async_trait]
impl FileHook for LimitPathLengthHook {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        _cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution, Error> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if change.is_none() {
            // You can always delete paths
            return Ok(HookExecution::Accepted);
        }

        // Encode file in mercurial encoding to make sure mercurial can accept those files
        // as well
        if let Some(rejection) = check_path(path)? {
            return Ok(rejection);
        }

        let len = path.len();

        let execution = if len >= self.length_limit {
            HookExecution::Rejected(HookRejectionInfo::new_long(
                "Path too long",
                format!(
                    "Path length for '{}' ({}) exceeds length limit (>= {})",
                    path, len, self.length_limit
                ),
            ))
        } else {
            HookExecution::Accepted
        };

        Ok(execution)
    }
}

fn check_path(path: &MPath) -> Result<Option<HookExecution>, Error> {
    let mut elements = path
        .as_ref()
        .iter()
        .map(|e| e.as_ref())
        .collect::<Vec<&[u8]>>();

    let basename = elements
        .pop()
        .ok_or_else(|| anyhow!("invalid path - no basename!"))?;

    let mut basename = Vec::from(basename);
    basename.extend(b".i");
    elements.push(&basename);

    let encoded_index_path = simple_fsencode(&elements);

    for component in encoded_index_path.iter() {
        if component.len() > MAX_PATH_COMPONENT_LIMIT {
            return Ok(Some(HookExecution::Rejected(HookRejectionInfo::new_long(
                "Path component too long",
                format!(
                    "Path component length for {:?} ({}) exceeds length limit (>= {})",
                    component,
                    component.len(),
                    MAX_PATH_COMPONENT_LIMIT,
                ),
            ))));
        }
    }

    Ok(None)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_path_bad() {
        let path = MPath::new("flib/intern/__generated__/GraphQLMeerkatStep/flib/intern/entschema/generated/entity/profile_plus/EntPlatformToolViewerContextCallsiteMigrationRuleAction.php/GQLG_Intern__PlatformToolViewerContextCallsiteMigrationRuleChangeRuleDescriptionResponsePayload__EntPlatformToolViewerContextCallsiteMigrationRuleAction__genPerformGraphQLPlatformToolViewerContextCallsiteMigrationRuleChangeRuleDescriptionMutationType.php").unwrap();
        assert!(check_path(&path).unwrap().is_some());
    }

    #[test]
    fn test_path_ok() {
        let path = MPath::new("flib/intern/__generated__/GraphQLFetchersMeerkatStep/ic/GQLG_File__EntIcxPositionSearchHitWorkdayPositionViewStateJunction__GraphQLFacebookInternalTypeSetFetcherWrapper.php").unwrap();
        assert!(check_path(&path).unwrap().is_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::fmt;

use anyhow::Error;
use regex::Regex;

enum TranslationOutput {
    Literal(String),
    LuaSpecial(String),
    CharClass(String),
}

fn lua_special_to_regex(s: &str) -> String {
    // see https://www.lua.org/pil/20.2.html
    // NOTE: If you add to this, you need to cross-check pattern_to_regex to ensure it
    // correctly records a special, not a literal
    match s {
        "." => ".".to_string(),
        "%a" => "[[:alpha:]]".to_string(),
        "%A" => "[[:^alpha:]]".to_string(),
        "%c" => "[[:cntrl:]]".to_string(),
        "%C" => "[[:^cntrl:]]".to_string(),
        "%d" => "[[:digit:]]".to_string(),
        "%D" => "[[:^digit:]]".to_string(),
        "%l" => "[[:lower:]]".to_string(),
        "%L" => "[[:^lower:]]".to_string(),
        "%p" => "[[:punct:]]".to_string(),
        "%P" => "[[:^punct:]]".to_string(),
        "%s" => "[[:space:]]".to_string(),
        "%S" => "[[:^space:]]".to_string(),
        "%u" => "[[:upper:]]".to_string(),
        "%U" => "[[:^upper:]]".to_string(),
        "%w" => "[[:word:]]".to_string(),
        "%W" => "[[:^word:]]".to_string(),
        "%x" => "[[:xdigit:]]".to_string(),
        "%X" => "[[:^xdigit:]]".to_string(),
        "%z" => "\\0".to_string(),
        "(" => "(".to_string(),
        ")" => ")".to_string(),
        "+" => "+".to_string(),
        "*" => "*".to_string(),
        "-" => "*?".to_string(),
        "?" => "?".to_string(),
        "^" => "^".to_string(),
        "$" => "$".to_string(),
        _ if s.starts_with('%') && s.len() > 1 => regex::escape(&s[1..]),
        _ => panic!("Untranslated pattern component"),
    }
}

#[derive(Clone, Copy)]
enum ParseState {
    Normal,
    LastPercent,
    CharClass,
    CharClassLiteral,
}

impl ParseState {
    fn is_char_class(&self) -> bool {
        use ParseState::*;
        match self {
            Normal => false,
            LastPercent => false,
            CharClass => true,
            CharClassLiteral => true,
        }
    }
}

fn pattern_to_regex(other: &str) -> String {
    let mut output = Vec::new();
    let mut current_acc = String::new();
    let mut state = ParseState::Normal;

    fn flush_acc(acc: &mut String, output: &mut Vec<TranslationOutput>) {
        let acc = std::mem::take(acc);
        if !acc.is_empty() {
            output.push(TranslationOutput::Literal(acc));
        }
    }

    fn flush_class(acc: &mut String, output: &mut Vec<TranslationOutput>) {
        let acc = std::mem::take(acc);
        output.push(TranslationOutput::CharClass(acc));
    }

    for c in other.chars() {
        match (state, c) {
            (ParseState::CharClass, ']') => {
                flush_class(&mut current_acc, &mut output);
                state = ParseState::Normal;
            }
            (ParseState::CharClass, '\\') => {
                state = ParseState::CharClassLiteral;
                current_acc.push(c);
            }
            (ParseState::CharClass, _) => current_acc.push(c),
            (ParseState::CharClassLiteral, _) => {
                state = ParseState::CharClass;
                current_acc.push(c);
            }
            (ParseState::LastPercent, _) => {
                state = ParseState::Normal;
                flush_acc(&mut current_acc, &mut output);
                let mut special = String::with_capacity(2);
                special.push('%');
                special.push(c);
                output.push(TranslationOutput::LuaSpecial(special));
            }
            (ParseState::Normal, '[') => {
                flush_acc(&mut current_acc, &mut output);
                state = ParseState::CharClass;
            }
            (ParseState::Normal, '%') => {
                state = ParseState::LastPercent;
            }
            (ParseState::Normal, _) if r".()+*-?^$".contains(c) => {
                flush_acc(&mut current_acc, &mut output);
                output.push(TranslationOutput::LuaSpecial(c.to_string()));
            }
            (ParseState::Normal, _) => current_acc.push(c),
        };
    }

    if state.is_char_class() {
        current_acc.insert(0, '[');
    }
    flush_acc(&mut current_acc, &mut output);

    output
        .into_iter()
        .map(|item| match item {
            TranslationOutput::Literal(s) => regex::escape(&s),
            TranslationOutput::LuaSpecial(s) => lua_special_to_regex(&s),
            TranslationOutput::CharClass(s) => format!("[{}]", s),
        })
        .collect()
}

pub struct LuaPattern {
    original: String,
    regex: Regex,
}

impl LuaPattern {
    pub fn is_match(&self, s: &str) -> bool {
        self.regex.is_match(s)
    }

    #[allow(dead_code)]
    pub fn get_regex(&self) -> &Regex {
        &self.regex
    }
}

impl TryFrom<&str> for LuaPattern {
    type Error = Error;
    fn try_from(other: &str) -> Result<Self, Error> {
        let regex = Regex::new(&pattern_to_regex(other))?;
        Ok(Self {
            original: other.to_string(),
            regex,
        })
    }
}

impl TryFrom<String> for LuaPattern {
    type Error = Error;
    fn try_from(other: String) -> Result<Self, Error> {
        let regex = Regex::new(&pattern_to_regex(&other))?;
        Ok(Self {
            original: other,
            regex,
        })
    }
}

impl fmt::Display for LuaPattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        self.original.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    // All tests represent something that went wrong in translation during manual testing
    use super::*;

    #[test]
    fn test_literal() {
        assert_eq!("hello", pattern_to_regex("hello"));
    }

    #[test]
    fn test_anchors() {
        assert_eq!("^str$", pattern_to_regex("^str$"));
        assert_eq!("^buck\\-out/", pattern_to_regex("^buck%-out/"));
    }

    #[test]
    fn test_escaped() {
        assert_eq!("/buck\\-out/", pattern_to_regex("/buck%-out/"));
    }

    #[test]
    fn test_class() {
        assert_eq!("[[:alpha:]]", pattern_to_regex("%a"));
        assert_eq!("[[:^digit:]]", pattern_to_regex("%D"));
        assert_eq!("[abc]", pattern_to_regex("[abc]"));
        assert_eq!("/[.]git/", pattern_to_regex("/[.]git/"));
        assert_eq!("^[.]git/", pattern_to_regex("^[.]git/"));
        assert_eq!("[ab\\]]", pattern_to_regex("[ab\\]]"));
    }

    #[test]
    fn test_matching() {
        let pattern: LuaPattern = "^[.]git/"
            .try_into()
            .expect("Could not map pattern to regex");
        assert!(pattern.is_match(".git/foo"));
        assert!(!pattern.is_match("./git/foo"));

        let pattern: LuaPattern = "^buck%-out/"
            .try_into()
            .expect("Could not map pattern to regex");
        assert!(pattern.is_match("buck-out/file"));
        assert!(!pattern.is_match("/buck-out/file"));
    }
}


/*
 * 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.
 */

//! For Facebook hooks check the src/facebook/ folder

mod always_fail_changeset;
mod block_empty_commit;
mod check_nocommit;
mod conflict_markers;
pub(crate) mod deny_files;
mod limit_commit_message_length;
pub(crate) mod limit_commitsize;
pub(crate) mod limit_filesize;
mod limit_path_length;
mod lua_pattern;
pub(crate) mod no_bad_extensions;
pub(crate) mod no_bad_filenames;
mod no_insecure_filenames;
pub(crate) mod no_questionable_filenames;
pub(crate) mod no_windows_filenames;

use anyhow::Result;
use fbinit::FacebookInit;
use futures::future::Future;
use metaconfig_types::HookConfig;
use permission_checker::AclProvider;
use permission_checker::ArcMembershipChecker;

pub(crate) use self::lua_pattern::LuaPattern;
use crate::ChangesetHook;
use crate::FileHook;

fn b(t: impl ChangesetHook + 'static) -> Box<dyn ChangesetHook> {
    Box::new(t)
}

// This function could be written using async/await syntactic sugar but it
// had to be desugarised because of a bug: https://github.com/rust-lang/rust/issues/63033
// It has to return impl Future to maintain compatibility with facebook implementation.
pub fn hook_name_to_changeset_hook<'a>(
    _fb: FacebookInit,
    name: &'a str,
    config: &'a HookConfig,
    _acl_provider: &'a dyn AclProvider,
    _reviewers_membership: ArcMembershipChecker,
    _repo_name: &str,
) -> impl Future<Output = Result<Option<Box<dyn ChangesetHook + 'static>>>> + 'a {
    async move {
        Ok(match name {
            "always_fail_changeset" => Some(b(always_fail_changeset::AlwaysFailChangeset::new())),
            "block_empty_commit" => Some(b(block_empty_commit::BlockEmptyCommit::new())),
            "check_nocommit_message" => Some(b(check_nocommit::CheckNocommitHook::new(config)?)),
            "limit_commit_message_length" => Some(b(
                limit_commit_message_length::LimitCommitMessageLength::new(config)?,
            )),
            "limit_commitsize" => Some(b(limit_commitsize::LimitCommitsize::builder()
                .set_from_config(config)
                .build()?)),
            _ => None,
        })
    }
}

pub fn hook_name_to_file_hook(
    _fb: FacebookInit,
    name: &str,
    config: &HookConfig,
) -> Result<Option<Box<dyn FileHook + 'static>>> {
    Ok(match name {
        "check_nocommit" => Some(Box::new(check_nocommit::CheckNocommitHook::new(config)?)),
        "conflict_markers" => Some(Box::new(conflict_markers::ConflictMarkers::new())),
        "deny_files" => Some(Box::new(
            deny_files::DenyFiles::builder()
                .set_from_config(config)
                .build()?,
        )),
        "limit_filesize" => Some(Box::new(
            limit_filesize::LimitFilesize::builder()
                .set_from_config(config)
                .build()?,
        )),
        "limit_path_length" => Some(Box::new(limit_path_length::LimitPathLengthHook::new(
            config,
        )?)),
        "no_bad_filenames" => Some(Box::new(
            no_bad_filenames::NoBadFilenames::builder()
                .set_from_config(config)
                .build()?,
        )),
        "no_bad_extensions" => Some(Box::new(
            no_bad_extensions::NoBadExtensions::builder()
                .set_from_config(config)
                .build()?,
        )),
        "no_insecure_filenames" => {
            Some(Box::new(no_insecure_filenames::NoInsecureFilenames::new()?))
        }
        "no_questionable_filenames" => Some(Box::new(
            no_questionable_filenames::NoQuestionableFilenames::builder()
                .set_from_config(config)
                .build()?,
        )),
        "no_windows_filenames" => Some(Box::new(
            no_windows_filenames::NoWindowsFilenames::builder()
                .set_from_config(config)
                .build()?,
        )),
        _ => 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 anyhow::anyhow;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use metaconfig_types::HookConfig;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct NoBadExtensionsBuilder {
    illegal_extensions: Option<Vec<String>>,
}

impl NoBadExtensionsBuilder {
    pub fn set_from_config(mut self, config: &HookConfig) -> Self {
        if let Some(v) = config.strings.get("illegal_extensions") {
            self = self.illegal_extensions(v.split(','))
        }
        self
    }

    pub fn illegal_extensions(mut self, strs: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
        self.illegal_extensions =
            Some(strs.into_iter().map(|s| String::from(s.as_ref())).collect());
        self
    }

    pub fn build(self) -> Result<NoBadExtensions> {
        Ok(NoBadExtensions {
            illegal_extensions: self
                .illegal_extensions
                .ok_or_else(|| anyhow!("Missing illegal_extensions config"))?,
        })
    }
}

pub struct NoBadExtensions {
    illegal_extensions: Vec<String>,
}

impl NoBadExtensions {
    pub fn builder() -> NoBadExtensionsBuilder {
        NoBadExtensionsBuilder::default()
    }
}

#[async_trait]
impl FileHook for NoBadExtensions {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        _cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if change.is_none() {
            return Ok(HookExecution::Accepted);
        }

        let path = format!("{}", path);
        let lowercase_path = path.to_lowercase();
        for ext in &self.illegal_extensions {
            if lowercase_path.ends_with(ext) {
                return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                    "Illegal filename",
                    format!(
                        "ABORT: Illegal filename: '{}'. You cannot commit {} files.",
                        path, ext
                    ),
                )));
            }
        }
        Ok(HookExecution::Accepted)
    }
}


/*
 * 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 anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use metaconfig_types::HookConfig;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;
use regex::Regex;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct NoBadFilenamesBuilder<'a> {
    allowlist_regex: Option<&'a str>,
    illegal_regex: Option<&'a str>,
}

impl<'a> NoBadFilenamesBuilder<'a> {
    pub fn set_from_config(mut self, config: &'a HookConfig) -> Self {
        if let Some(v) = config.strings.get("allowlist_regex") {
            self = self.allowlist_regex(v)
        }
        if let Some(v) = config.strings.get("illegal_regex") {
            self = self.illegal_regex(v)
        }
        self
    }

    pub fn allowlist_regex(mut self, regex: &'a str) -> Self {
        self.allowlist_regex = Some(regex);
        self
    }

    pub fn illegal_regex(mut self, regex: &'a str) -> Self {
        self.illegal_regex = Some(regex);
        self
    }

    pub fn build(self) -> Result<NoBadFilenames> {
        Ok(NoBadFilenames {
            allowlist_regex: self
                .allowlist_regex
                .map(Regex::new)
                .transpose()
                .context("Failed to create regex for allowlist")?,
            illegal_regex: Regex::new(
                self.illegal_regex
                    .ok_or_else(|| anyhow!("Missing illegal_regex config"))?,
            )
            .context("Failed to create regex for illegal")?,
        })
    }
}

pub struct NoBadFilenames {
    allowlist_regex: Option<Regex>,
    illegal_regex: Regex,
}

impl NoBadFilenames {
    pub fn builder<'a>() -> NoBadFilenamesBuilder<'a> {
        NoBadFilenamesBuilder::default()
    }
}

#[async_trait]
impl FileHook for NoBadFilenames {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        _cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if change.is_none() {
            return Ok(HookExecution::Accepted);
        }

        let path = format!("{}", path);
        if self.illegal_regex.is_match(&path) {
            match self.allowlist_regex {
                Some(ref allow) if allow.is_match(&path) => {}
                _ => {
                    return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                        "Illegal filename",
                        format!(
                            "ABORT: Illegal filename: '{}'. Filenames must not match '{}'.",
                            path, self.illegal_regex
                        ),
                    )));
                }
            }
        }
        Ok(HookExecution::Accepted)
    }
}


/*
 * 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 anyhow::Error;
use async_trait::async_trait;
use context::CoreContext;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;
use regex::Regex;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

pub struct NoInsecureFilenames {
    illegal_regex: Regex,
    insecure_lower_regex: Regex,
    insecure_regex: Vec<Regex>,
}

impl NoInsecureFilenames {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            // .hgtags files break remotefilelog and .hgsub/.hgsubstate break hg-git,
            illegal_regex: Regex::new(r"(^.hg(tags|sub|substate)$)")?,
            // Block commits that attempt to exploit CVE-2015-9390:
            // http://git-blame.blogspot.com.es/2014/12/git-1856-195-205-214-and-221-and.html.
            // There are three vulnerabilities here:
            // 1. files like .Git/config (on all case-insensitive file systems)
            // 2. files like .GIT~1/config (on Windows)
            // 3. files containing one out of 16 UTF-8 encoded Unicode characters that are ignorable (on OS X)
            //
            // Also block the .hg and .svn equivalents.
            insecure_lower_regex: Regex::new(r"(^|/)\.(git|hg|svn)((8b6c)?\~[0-9]+)?(/|$)")?,
            insecure_regex: vec![
                Regex::new(
                    r"\x{200b}|\x{200c}|\x{200d}|\x{200e}|\x{200f}|\x{202a}|\x{202b}|\x{202c}0",
                )?,
                Regex::new(
                    r"\x{202d}|\x{202e}|\x{206a}|\x{206b}|\x{206c}|\x{206d}|\x{206e}|\x{feff}",
                )?,
            ],
        })
    }
}

#[async_trait]
impl FileHook for NoInsecureFilenames {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        _cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution, Error> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if change.is_none() {
            return Ok(HookExecution::Accepted);
        }

        let path = format!("{}", path);
        let lower_path = path.to_lowercase();
        if self.illegal_regex.is_match(&lower_path) {
            return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                "Illegal filename",
                format!("ABORT: Illegal filename: {}", path),
            )));
        }

        if self.insecure_lower_regex.is_match(&lower_path) {
            return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                "Illegal insecure name",
                format!("ABORT: Illegal insecure name: {}", path),
            )));
        }

        for insecure_regex in &self.insecure_regex {
            if insecure_regex.is_match(&path) {
                return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                    "Illegal insecure name",
                    format!("ABORT: Illegal insecure name: {}", path),
                )));
            }
        }
        Ok(HookExecution::Accepted)
    }
}


/*
 * 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 anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use metaconfig_types::HookConfig;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;
use regex::Regex;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct NoQuestionableFilenamesBuilder<'a> {
    allowlist_for_braces: Option<&'a str>,
    allowlist_for_cmd_line: Option<&'a str>,
}

impl<'a> NoQuestionableFilenamesBuilder<'a> {
    pub fn set_from_config(mut self, config: &'a HookConfig) -> Self {
        if let Some(v) = config.strings.get("allowlist_for_braces") {
            self.allowlist_for_braces = Some(v);
        }
        if let Some(v) = config.strings.get("allowlist_for_cmd_line") {
            self.allowlist_for_cmd_line = Some(v);
        }
        self
    }

    pub fn build(self) -> Result<NoQuestionableFilenames> {
        Ok(NoQuestionableFilenames {
            allowlist_for_braces: self
                .allowlist_for_braces
                .map(Regex::new)
                .transpose()
                .context("Failed to create allowlist regex for braces")?,
            braces: Regex::new(r"[{}]")?,
            allowlist_for_cmd_line: self
                .allowlist_for_cmd_line
                .map(Regex::new)
                .transpose()
                .context("Failed to create allowlist regex for cmd_line")?,
            // Disallow spaces, apostrophes, and files that start with hyphens
            cmd_line: Regex::new(r"\s|'|(^|/)-")?,
        })
    }
}

pub struct NoQuestionableFilenames {
    allowlist_for_braces: Option<Regex>,
    braces: Regex,
    allowlist_for_cmd_line: Option<Regex>,
    cmd_line: Regex,
}

impl NoQuestionableFilenames {
    pub fn builder<'a>() -> NoQuestionableFilenamesBuilder<'a> {
        NoQuestionableFilenamesBuilder::default()
    }
}

#[async_trait]
impl FileHook for NoQuestionableFilenames {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _content_manager: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if cross_repo_push_source == CrossRepoPushSource::PushRedirected {
            // For push-redirected pushes we rely on the hook
            // running in the original repo
            return Ok(HookExecution::Accepted);
        }
        if change.is_none() {
            return Ok(HookExecution::Accepted);
        }

        let path = format!("{}", path);
        if self.braces.is_match(&path) {
            match self.allowlist_for_braces {
                Some(ref allow) if allow.is_match(&path) => {}
                _ => {
                    return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                        "Illegal filename",
                        format!(
                            "ABORT: Illegal filename: {}. The file name cannot include brace(s).",
                            path
                        ),
                    )));
                }
            }
        }

        if self.cmd_line.is_match(&path) {
            match self.allowlist_for_cmd_line {
                Some(ref allow) if allow.is_match(&path) => {}
                _ => {
                    return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                        "Illegal filename",
                        format!(
                            "ABORT: Illegal filename: {}. The file name cannot include spaces, apostrophes or start with hyphens.",
                            path
                        ),
                    )));
                }
            }
        }

        Ok(HookExecution::Accepted)
    }
}


/*
 * 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 anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use metaconfig_types::HookConfig;
use mononoke_types::BasicFileChange;
use mononoke_types::MPath;
use regex::bytes::Regex;

use crate::CrossRepoPushSource;
use crate::FileContentManager;
use crate::FileHook;
use crate::HookExecution;
use crate::HookRejectionInfo;
use crate::PushAuthoredBy;

#[derive(Default)]
pub struct NoWindowsFilenamesBuilder<'a> {
    /// Paths on which bad Windows filenames are not disallowed.
    allowed_paths: Option<&'a str>,
}

impl<'a> NoWindowsFilenamesBuilder<'a> {
    pub fn set_from_config(mut self, config: &'a HookConfig) -> Self {
        if let Some(v) = config.strings.get("allowed_paths") {
            self = self.allowed_paths(v)
        }

        self
    }

    pub fn allowed_paths(mut self, regex: &'a str) -> Self {
        self.allowed_paths = Some(regex);
        self
    }

    pub fn build(self) -> Result<NoWindowsFilenames> {
        Ok(NoWindowsFilenames {
            allowed_paths: self
                .allowed_paths
                .map(Regex::new)
                .transpose()
                .context("Failed to create allowed_paths regex")?,
        })
    }
}

pub struct NoWindowsFilenames {
    allowed_paths: Option<Regex>,
}

/// Hook to disallow bad Windows filenames from being pushed.
///
/// These bad filenames are described by Microsoft as:
///  "CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3,
///  LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9. Also avoid these names followed immediately by an
///  extension; for example, NUL.txt is not recommended. For more information, see Namespaces."
///
///  In addition the following characters are invalid: <>:"/\|?* and the chars 0-31
///
///  The filename shouldn't end with space or period. Windows shell and UX don't support such files.
///
///  More info: https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file
impl NoWindowsFilenames {
    pub fn builder<'a>() -> NoWindowsFilenamesBuilder<'a> {
        NoWindowsFilenamesBuilder::default()
    }
}

const BAD_WINDOWS_PATH_ELEMENT_REGEX: &str =
    r#"(^(?i)((((com|lpt)\d)|con|prn|aux|nul))($|\.))|<|>|:|"|/|\\|\||\?|\*|[\x00-\x1F]|(\.| )$"#;

fn check_path_for_bad_elements(path: &MPath) -> Result<HookExecution, Error> {
    let bad_windows_path_element = Regex::new(BAD_WINDOWS_PATH_ELEMENT_REGEX)?;
    for element in path {
        if bad_windows_path_element.is_match(element.as_ref()) {
            return Ok(HookExecution::Rejected(HookRejectionInfo::new_long(
                "Illegal windows filename",
                format!("ABORT: Illegal windows filename: {}", element),
            )));
        }
    }
    Ok(HookExecution::Accepted)
}

#[async_trait]
impl FileHook for NoWindowsFilenames {
    async fn run<'this: 'change, 'ctx: 'this, 'change, 'fetcher: 'change, 'path: 'change>(
        &'this self,
        _ctx: &'ctx CoreContext,
        _context_fetcher: &'fetcher dyn FileContentManager,
        change: Option<&'change BasicFileChange>,
        path: &'path MPath,
        cross_repo_push_source: CrossRepoPushSource,
        push_authored_by: PushAuthoredBy,
    ) -> Result<HookExecution> {
        if push_authored_by.service() {
            return Ok(HookExecution::Accepted);
        }
        if cross_repo_push_source == CrossRepoPushSource::PushRedirected {
            // For push-redirected pushes we rely on the hook
            // running in the original repo
            return Ok(HookExecution::Accepted);
        }

        if change.is_none() {
            return Ok(HookExecution::Accepted);
        }

        if let Some(allowed_paths) = &self.allowed_paths {
            if allowed_paths.is_match(&path.to_vec()) {
                return Ok(HookExecution::Accepted);
            }
        }

        Ok(check_path_for_bad_elements(path)?)
    }
}

/// This test is only testing the `check_path_for_bad_elements`, the rest of the
/// hook is only tested through integration tests.
#[cfg(test)]
mod test {
    use super::*;

    fn check_path(path: &str) -> bool {
        match check_path_for_bad_elements(&MPath::new(path).unwrap()).unwrap() {
            HookExecution::Accepted => true,
            HookExecution::Rejected(_) => false,
        }
    }

    #[test]
    fn test_good_paths() {
        assert!(check_path("dir/some_filename.exe"));
        assert!(check_path(
            "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_long_filename"
        ));
        assert!(check_path("aaa/LPT2137"));
        assert!(check_path("COM"));
        assert!(check_path("spaces are allowed!"));
        assert!(check_path("x/y/z/file_with_tildle~_in_the_name"));
    }

    #[test]
    fn invalid_chars() {
        assert!(!check_path("x/y/z/file_with_backslash\\_in_the_name"));
        assert!(!check_path("x/y/dir_with_pipe|in_the_name/file"));
        assert!(!check_path("x/y/dir_with_less_than<in_the_name/file"));
    }

    #[test]
    fn invalid_names() {
        assert!(!check_path("aaa/COm1"));
        assert!(!check_path("aaa/lPt3"));
        assert!(!check_path("x/CON/file"));
        assert!(!check_path("x/y/AUX"));
        assert!(!check_path("NUL.txt"));
        assert!(!check_path("COM1.txt"));
    }
}


/*
 * 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::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;

use anyhow::anyhow;
use anyhow::Result;
use blobstore::Loadable;
use bookmarks_movement::BookmarkKindRestrictions;
use cloned::cloned;
use futures::future::TryFutureExt;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use hooks::CrossRepoPushSource;
use land_service_if::types::*;
use mononoke_api::CoreContext;
use mononoke_api::MononokeError;
use mononoke_api::RepoContext;
use mononoke_types::private::Bytes;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use pushrebase::PushrebaseChangesetPair;
use repo_blobstore::RepoBlobstoreRef;

use crate::errors;
use crate::errors::LandChangesetsError;

/// Convert BTreeSet of ChangetSetIds to a Hashset of BonsaiChangeset
pub async fn convert_bonsai_changesets(
    changesets: BTreeSet<Vec<u8>>,
    ctx: &CoreContext,
    repo: &RepoContext,
) -> Result<HashSet<BonsaiChangeset>, LandChangesetsError> {
    let blobstore = repo.blob_repo().repo_blobstore();
    let changeset_ids = changesets
        .into_iter()
        .map(convert_changeset_id_from_bytes)
        .collect::<Result<HashSet<_>, LandChangesetsError>>()?;

    let changesets: HashSet<BonsaiChangeset> = stream::iter(changeset_ids)
        .map(|cs_id| {
            cloned!(ctx);
            async move {
                cs_id
                    .load(&ctx, blobstore)
                    .map_err(MononokeError::from)
                    .await
            }
        })
        .buffer_unordered(100)
        .try_collect()
        .await?;
    Ok(changesets)
}

pub fn convert_changeset_id_from_bytes(
    bonsai: Vec<u8>,
) -> Result<ChangesetId, LandChangesetsError> {
    Ok(ChangesetId::from_bytes(bonsai)?)
}

/// Convert a pushvars map from thrift's representation to the one used
/// internally in mononoke.
pub(crate) fn convert_pushvars(pushvars: BTreeMap<String, Vec<u8>>) -> HashMap<String, Bytes> {
    pushvars
        .into_iter()
        .map(|(name, value)| (name, Bytes::from(value)))
        .collect()
}

pub(crate) fn convert_hex_to_str(changeset: &[u8]) -> String {
    faster_hex::hex_string(changeset)
}

/// Convert bookmark restrictions from the bookmark in the request
pub fn convert_bookmark_restrictions(
    bookmark_restrictions: land_service_if::BookmarkKindRestrictions,
) -> Result<BookmarkKindRestrictions, LandChangesetsError> {
    match bookmark_restrictions {
        land_service_if::BookmarkKindRestrictions::ANY_KIND => {
            Ok(BookmarkKindRestrictions::AnyKind)
        }
        land_service_if::BookmarkKindRestrictions::ONLY_SCRATCH => {
            Ok(BookmarkKindRestrictions::OnlyScratch)
        }
        land_service_if::BookmarkKindRestrictions::ONLY_PUBLISHING => {
            Ok(BookmarkKindRestrictions::OnlyPublishing)
        }
        other => Err(LandChangesetsError::InternalError(errors::internal_error(
            anyhow!("Unknown BookmarkKindRestrictions: {}", other).as_ref(),
        ))),
    }
}

/// Convert cross repo push source from the cross_repo_push_source in the request
pub fn convert_cross_repo_push_source(
    cross_repo_push_source: land_service_if::CrossRepoPushSource,
) -> Result<CrossRepoPushSource, LandChangesetsError> {
    match cross_repo_push_source {
        land_service_if::CrossRepoPushSource::NATIVE_TO_THIS_REPO => {
            Ok(CrossRepoPushSource::NativeToThisRepo)
        }
        land_service_if::CrossRepoPushSource::PUSH_REDIRECTED => {
            Ok(CrossRepoPushSource::PushRedirected)
        }
        other => Err(LandChangesetsError::InternalError(errors::internal_error(
            anyhow!("Unknown CrossRepoPushSource: {}", other).as_ref(),
        ))),
    }
}

/// Convert vec of PushrebaseChangesetPair and converts it to a vec of BonsaiHashPairs
pub fn convert_rebased_changesets_into_pairs(
    rebased_changeset: PushrebaseChangesetPair,
) -> BonsaiHashPairs {
    BonsaiHashPairs {
        old_id: rebased_changeset.id_old.as_ref().to_vec(),
        new_id: rebased_changeset.id_new.as_ref().to_vec(),
    }
}

/// Convert usize and to i64
pub fn convert_to_i64(val: usize) -> Result<i64, LandChangesetsError> {
    val.try_into()
        .map_err(|_| anyhow!("usize too big for i64").into())
}

/// Converts option of ChangesetId to vec binary used in thrift to represent ChangesetId
pub fn convert_changeset_id_to_vec_binary(
    old_bookmark_value: ChangesetId,
) -> land_service_if::ChangesetId {
    old_bookmark_value.as_ref().to_vec()
}


/*
 * 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::error::Error as StdError;

use anyhow::Error;
use bookmarks_movement::describe_hook_rejections;
use bookmarks_movement::BookmarkMovementError;
use bookmarks_movement::HookRejection;
use land_service_if::services::land_service::LandChangesetsExn;
use land_service_if::InternalError;
use mononoke_api::MononokeError;
use pushrebase::PushrebaseConflict;
use pushrebase::PushrebaseError;
use thiserror::Error as ThisError;

#[derive(ThisError, Debug, Clone)]
pub enum LandChangesetsError {
    #[error("Internal error: {0}")]
    InternalError(land_service_if::InternalError),
    #[error("Conflicts while pushrebasing: {0:?}")]
    PushrebaseConflicts(Vec<pushrebase::PushrebaseConflict>),
    #[error("Hooks failed:\n{}", describe_hook_rejections(.0.as_slice()))]
    HookRejections(Vec<HookRejection>),
}

impl From<land_service_if::InternalError> for LandChangesetsError {
    fn from(e: InternalError) -> Self {
        LandChangesetsError::InternalError(internal_error(&e))
    }
}

impl From<MononokeError> for LandChangesetsError {
    fn from(e: MononokeError) -> Self {
        match e {
            MononokeError::HookFailure(rejections) => Self::HookRejections(rejections),
            MononokeError::PushrebaseConflicts(conflicts) => Self::PushrebaseConflicts(conflicts),
            e => LandChangesetsError::InternalError(internal_error(&e)),
        }
    }
}

impl From<BookmarkMovementError> for LandChangesetsError {
    fn from(e: BookmarkMovementError) -> Self {
        match e {
            BookmarkMovementError::HookFailure(rejections) => Self::HookRejections(rejections),
            BookmarkMovementError::PushrebaseError(PushrebaseError::Conflicts(conflicts)) => {
                Self::PushrebaseConflicts(conflicts)
            }
            e => LandChangesetsError::InternalError(internal_error(&e)),
        }
    }
}

impl From<anyhow::Error> for LandChangesetsError {
    fn from(e: Error) -> Self {
        LandChangesetsError::InternalError(internal_error(e.as_ref()))
    }
}

impl From<LandChangesetsError> for LandChangesetsExn {
    fn from(e: LandChangesetsError) -> LandChangesetsExn {
        match e {
            LandChangesetsError::InternalError(e) => {
                land_service_if::services::land_service::LandChangesetsExn::internal_error(e)
            }
            LandChangesetsError::HookRejections(rejections) => {
                LandChangesetsExn::hook_rejections(land_service_if::HookRejectionsException {
                    reason: reason_rejections(&rejections),
                    rejections: rejections.into_iter().map(convert_rejection).collect(),
                })
            }
            LandChangesetsError::PushrebaseConflicts(conflicts) => {
                LandChangesetsExn::pushrebase_conflicts(
                    land_service_if::PushrebaseConflictsException {
                        reason: reason_conflicts(&conflicts),
                        conflicts: conflicts
                            .into_iter()
                            .map(|c| land_service_if::PushrebaseConflicts {
                                left: c.left.to_string(),
                                right: c.right.to_string(),
                            })
                            .collect(),
                    },
                )
            }
        }
    }
}

pub(crate) fn internal_error(error: &dyn StdError) -> land_service_if::InternalError {
    let _reason = format!("{:#}", error);
    let mut source_chain = Vec::new();
    let mut error: &dyn StdError = &error;
    while let Some(source) = error.source() {
        source_chain.push(source.to_string());
        error = source;
    }
    land_service_if::InternalError {
        reason: error.to_string(),
        backtrace: None,
        source_chain,
    }
}

fn reason_conflicts(conflicts: &Vec<PushrebaseConflict>) -> String {
    format!("Conflicts while pushrebasing: {:?}", conflicts)
}

fn convert_rejection(rejection: HookRejection) -> land_service_if::HookRejection {
    land_service_if::HookRejection {
        hook_name: rejection.hook_name,
        cs_id: Vec::from(rejection.cs_id.as_ref()),
        reason: land_service_if::HookOutcomeRejected {
            description: rejection.reason.description.to_string(),
            long_description: rejection.reason.long_description,
        },
    }
}

fn reason_rejections(rejections: &Vec<HookRejection>) -> String {
    format!(
        "Hooks failed:\n{}",
        describe_hook_rejections(rejections.as_slice())
    )
}


/*
 * 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::sync::Arc;

use async_trait::async_trait;
use fb303_core::fb303_status;
use fb303_core::server::BaseService;
use fb303_core::services::base_service::GetNameExn;
use fb303_core::services::base_service::GetStatusDetailsExn;
use fb303_core::services::base_service::GetStatusExn;

#[derive(Clone)]
pub struct BaseServiceImpl {
    will_exit: Arc<AtomicBool>,
}

impl BaseServiceImpl {
    pub fn new(will_exit: Arc<AtomicBool>) -> Self {
        Self { will_exit }
    }
}

#[async_trait]
impl BaseService for BaseServiceImpl {
    async fn getName(&self) -> Result<String, GetNameExn> {
        Ok("Land Service Server".to_string())
    }

    async fn getStatus(&self) -> Result<fb303_status, GetStatusExn> {
        if !self.will_exit.load(Ordering::Relaxed) {
            Ok(fb303_status::ALIVE)
        } else {
            Ok(fb303_status::STOPPING)
        }
    }

    async fn getStatusDetails(&self) -> Result<String, GetStatusDetailsExn> {
        if !self.will_exit.load(Ordering::Relaxed) {
            Ok("Alive and running.".to_string())
        } else {
            Ok("Shutting down.".to_string())
        }
    }
}


/*
 * 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::net::IpAddr;
use std::sync::Arc;

use anyhow::Result;
use fbinit::FacebookInit;
use identity::Identity;
use login_objects_thrift::EnvironmentType;
use metaconfig_types::CommonConfig;
use metadata::Metadata;
use mononoke_api::CoreContext;
use mononoke_api::Mononoke;
use mononoke_api::SessionContainer;
use permission_checker::MononokeIdentity;
use permission_checker::MononokeIdentitySet;
use scribe_ext::Scribe;
use scuba_ext::MononokeScubaSampleBuilder;
use scuba_ext::ScubaValue;
use slog::Logger;
use srserver::RequestContext;

use crate::errors;
use crate::errors::LandChangesetsError;

const FORWARDED_IDENTITIES_HEADER: &str = "scm_forwarded_identities";
const FORWARDED_CLIENT_IP_HEADER: &str = "scm_forwarded_client_ip";
const FORWARDED_CLIENT_DEBUG_HEADER: &str = "scm_forwarded_client_debug";
const FORWARDED_OTHER_CATS_HEADER: &str = "scm_forwarded_other_cats";

#[derive(Clone)]
pub(crate) struct Factory {
    pub(crate) fb: FacebookInit,
    pub(crate) logger: Logger,
    pub(crate) scuba_builder: MononokeScubaSampleBuilder,
    pub(crate) mononoke: Arc<Mononoke>,
    pub(crate) scribe: Scribe,
    pub(crate) identity: Identity,
}

impl Factory {
    pub fn new(
        fb: FacebookInit,
        logger: Logger,
        mononoke: Arc<Mononoke>,
        mut scuba_builder: MononokeScubaSampleBuilder,
        scribe: Scribe,
        common_config: &CommonConfig,
    ) -> Self {
        scuba_builder.add_common_server_data();

        Self {
            fb,
            logger,
            mononoke,
            scuba_builder,
            scribe,
            identity: Identity::new(
                common_config.internal_identity.id_type.as_str(),
                common_config.internal_identity.id_data.as_str(),
            ),
        }
    }

    // Create context from given name and request context
    pub(crate) async fn create_ctx(
        &self,
        name: &str,
        req_ctxt: &RequestContext,
    ) -> Result<CoreContext, LandChangesetsError> {
        let session = self.create_session(req_ctxt).await?;
        let identities = session.metadata().identities();
        let scuba = self.create_scuba(name, req_ctxt, identities)?;
        let ctx = session.new_context_with_scribe(self.logger.clone(), scuba, self.scribe.clone());
        Ok(ctx)
    }

    /// Create and configure a scuba sample builder for a request.
    fn create_scuba(
        &self,
        name: &str,
        req_ctxt: &RequestContext,
        identities: &MononokeIdentitySet,
    ) -> Result<MononokeScubaSampleBuilder, LandChangesetsError> {
        let mut scuba = self.scuba_builder.clone().with_seq("seq");
        scuba.add("type", "thrift");
        scuba.add("method", name);

        const CLIENT_HEADERS: &[&str] = &[
            "client_id",
            "client_type",
            "client_correlator",
            "proxy_client_id",
        ];
        for &header in CLIENT_HEADERS.iter() {
            let value = req_ctxt.header(header)?;
            if let Some(value) = value {
                scuba.add(header, value);
            }
        }

        scuba.add(
            "identities",
            identities
                .iter()
                .map(|id| id.to_string())
                .collect::<ScubaValue>(),
        );

        Ok(scuba)
    }

    async fn create_metadata(
        &self,
        req_ctxt: &RequestContext,
    ) -> Result<Metadata, LandChangesetsError> {
        let header = |h: &str| {
            req_ctxt
                .header(h)
                .map_err(|e| errors::internal_error(e.as_ref()))
        };

        let tls_identities: MononokeIdentitySet = req_ctxt
            .identities()?
            .entries()
            .into_iter()
            .map(MononokeIdentity::from_identity_ref)
            .collect();

        // Get any valid CAT identities.
        let cats_identities: MononokeIdentitySet = req_ctxt
            .identities_cats(
                &self.identity,
                &[EnvironmentType::PROD, EnvironmentType::CORP],
            )?
            .entries()
            .into_iter()
            .map(MononokeIdentity::from_identity_ref)
            .collect();

        if let (Some(forwarded_identities), Some(forwarded_ip)) = (
            header(FORWARDED_IDENTITIES_HEADER)?,
            header(FORWARDED_CLIENT_IP_HEADER)?,
        ) {
            let mut header_identities: MononokeIdentitySet =
                serde_json::from_str(forwarded_identities.as_str())
                    .map_err(|e| errors::internal_error(&e))?;
            let client_ip = Some(
                forwarded_ip
                    .parse::<IpAddr>()
                    .map_err(|e| errors::internal_error(&e))?,
            );
            let client_debug = header(FORWARDED_CLIENT_DEBUG_HEADER)?.is_some();

            header_identities.extend(cats_identities.into_iter());
            let mut metadata = Metadata::new(
                None,
                header_identities,
                client_debug,
                metadata::security::is_client_untrusted(|h| req_ctxt.header(h))?,
                client_ip,
            )
            .await;

            metadata.add_original_identities(tls_identities);

            if let Some(other_cats) = header(FORWARDED_OTHER_CATS_HEADER)? {
                metadata.add_raw_encoded_cats(other_cats);
            }

            return Ok(metadata);
        }

        Ok(Metadata::new(
            None,
            tls_identities.union(&cats_identities).cloned().collect(),
            false,
            metadata::security::is_client_untrusted(|h| req_ctxt.header(h))?,
            None,
        )
        .await)
    }

    /// Create and configure the session container for a request.
    async fn create_session(
        &self,
        req_ctxt: &RequestContext,
    ) -> Result<SessionContainer, LandChangesetsError> {
        let metadata = self.create_metadata(req_ctxt).await?;
        let session = SessionContainer::builder(self.fb)
            .metadata(Arc::new(metadata))
            .build();
        Ok(session)
    }
}


/*
 * 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::Arc;

use identity::Identity;
use land_service_if::LandChangesetRequest;
use mononoke_api::CoreContext;
use mononoke_api::Mononoke;

#[derive(Clone)]
pub struct LandChangesetObject {
    pub mononoke: Arc<Mononoke>,
    pub identity: Identity,
    pub ctx: CoreContext,
    pub request: LandChangesetRequest,
}

impl LandChangesetObject {
    pub fn new(
        mononoke: Arc<Mononoke>,
        identity: Identity,
        ctx: CoreContext,
        land_changesets: LandChangesetRequest,
    ) -> Self {
        Self {
            mononoke,
            identity,
            ctx,
            request: land_changesets,
        }
    }
}


/*
 * 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::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use futures::channel::oneshot;
use futures::future::BoxFuture;
use futures::future::Shared;
use land_service_if::server::LandService;
use land_service_if::services::land_service::LandChangesetsExn;
use land_service_if::types::*;
use mononoke_api::CoreContext;
use parking_lot::Mutex;
use srserver::RequestContext;
use tunables::tunables;

use crate::errors;
use crate::errors::LandChangesetsError;
use crate::factory::Factory;
use crate::land_changeset_object::LandChangesetObject;
use crate::worker;
use crate::worker::EnqueueSender;

#[derive(Clone)]
pub(crate) struct LandServiceImpl {
    factory: Factory,
    #[allow(dead_code)]
    repo_bookmark_map:
        Arc<Mutex<HashMap<RepoBookmarkKey, (EnqueueSender, Shared<BoxFuture<'static, ()>>)>>>,
}

pub(crate) struct LandServiceThriftImpl(LandServiceImpl);

impl LandServiceImpl {
    pub fn new(factory: Factory) -> Self {
        let repo_bookmark_map = Arc::new(Mutex::new(HashMap::new()));
        Self {
            factory,
            repo_bookmark_map,
        }
    }

    pub(crate) fn thrift_server(&self) -> LandServiceThriftImpl {
        LandServiceThriftImpl(self.clone())
    }
}

#[derive(Hash, PartialEq, Eq)]
struct RepoBookmarkKey {
    repo_name: String,
    bookmark: String,
}

#[async_trait]
impl LandService for LandServiceThriftImpl {
    type RequestContext = RequestContext;

    async fn land_changesets(
        &self,
        req_ctxt: &RequestContext,
        land_changesets: LandChangesetRequest,
    ) -> Result<LandChangesetsResponse, LandChangesetsExn> {
        let ctx: CoreContext = self
            .0
            .factory
            .create_ctx("land_changesets", req_ctxt)
            .await?;

        // Create an object with all required info to process a request
        let land_changeset_object = LandChangesetObject::new(
            self.0.factory.mononoke.clone(),
            self.0.factory.identity.clone(),
            ctx,
            land_changesets.clone(),
        );

        if tunables().batching_to_land_service().unwrap_or_default() {
            let (sender, receiver) =
                oneshot::channel::<Result<LandChangesetsResponse, LandChangesetsError>>();

            let serialized_key = RepoBookmarkKey {
                repo_name: land_changeset_object.request.repo_name.clone(),
                bookmark: land_changeset_object.request.bookmark.clone(),
            };

            let (worker_sender, worker_process_future) = {
                let mut repo_bookmark_map = self.0.repo_bookmark_map.lock();

                repo_bookmark_map
                    .entry(serialized_key)
                    .or_insert_with(worker::setup_worker)
                    .clone()
            };

            worker_sender
                .unbounded_send((sender, land_changeset_object.clone()))
                .map_err(|e| errors::internal_error(&e))?;

            worker_process_future.await;

            return Ok(receiver.await.map_err(|e| errors::internal_error(&e))??);
        }

        Ok(worker::impl_land_changesets(land_changeset_object).await?)
    }
}


/*
 * 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::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use clap::Parser;
use cloned::cloned;
use cmdlib_logging::ScribeLoggingArgs;
use fb303_core::server::make_BaseService_server;
use fbinit::FacebookInit;
use land_service_if::server::*;
use metaconfig_types::ShardedService;
use mononoke_app::args::ShutdownTimeoutArgs;
use mononoke_app::MononokeAppBuilder;
use slog::info;
use srserver::service_framework::BuildModule;
use srserver::service_framework::Fb303Module;
use srserver::service_framework::ServiceFramework;
use srserver::service_framework::ThriftStatsModule;
use srserver::ThriftServer;
use srserver::ThriftServerBuilder;
use tokio::task;
use LandService_metadata_sys::create_metadata;

const SERVICE_NAME: &str = "mononoke_land_service";

mod conversion_helpers;
mod errors;
mod facebook;
mod factory;
mod land_changeset_object;
mod land_service_impl;
mod scuba_request;
mod scuba_response;
mod worker;

#[derive(Debug, Parser)]
struct LandServiceServerArgs {
    #[clap(flatten)]
    shutdown_timeout_args: ShutdownTimeoutArgs,
    #[clap(flatten)]
    scribe_logging_args: ScribeLoggingArgs,
    /// Thrift host
    #[clap(long, short = 'H', default_value = "::")]
    host: String,
    /// Thrift port
    #[clap(long, short = 'p', default_value_t = 8485)]
    port: u16,
    /// Path for file in which to write the bound tcp address in rust std::net::SocketAddr format
    #[clap(long)]
    bound_address_file: Option<String>,
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<(), Error> {
    let app = MononokeAppBuilder::new(fb).build::<LandServiceServerArgs>()?;

    // Process commandline flags
    let args: LandServiceServerArgs = app.args()?;

    let logger = app.logger().clone();
    let runtime = app.runtime();
    let exec = runtime.clone();
    let env = app.environment();

    let scuba_builder = env.scuba_sample_builder.clone();
    let mononoke = Arc::new(
        runtime
            .block_on(app.open_managed_repos(Some(ShardedService::LandService)))?
            .make_mononoke_api()?,
    );

    let will_exit = Arc::new(AtomicBool::new(false));

    let fb303_base = {
        cloned!(will_exit);
        move |proto| {
            make_BaseService_server(proto, facebook::BaseServiceImpl::new(will_exit.clone()))
        }
    };

    let factory = factory::Factory::new(
        fb,
        logger.clone(),
        mononoke,
        scuba_builder,
        args.scribe_logging_args.get_scribe(fb)?,
        &app.repo_configs().common,
    );

    let land_service_server = land_service_impl::LandServiceImpl::new(factory);

    let service = {
        move |proto| {
            make_LandService_server(
                proto,
                land_service_server.thrift_server(),
                fb303_base.clone(),
            )
        }
    };

    let thrift: ThriftServer = ThriftServerBuilder::new(fb)
        .with_name(SERVICE_NAME)
        .expect("failed to set name")
        .with_address(&args.host, args.port, false)?
        .with_tls()
        .expect("failed to enable TLS")
        .with_cancel_if_client_disconnected()
        .with_metadata(create_metadata())
        .with_factory(exec, move || service)
        .build();

    let mut service_framework = ServiceFramework::from_server(SERVICE_NAME, thrift)
        .context("Failed to create service framework server")?;

    service_framework.add_module(BuildModule)?;
    service_framework.add_module(ThriftStatsModule)?;
    service_framework.add_module(Fb303Module)?;

    service_framework
        .serve_background()
        .expect("failed to start thrift service");

    let bound_addr = format!(
        "{}:{}",
        &args.host,
        service_framework.get_address()?.get_port()?
    );

    info!(logger, "Listening on {}", bound_addr);

    // Write out the bound address if requested, this is helpful in tests when using automatic binding with :0
    if let Some(bound_addr_path) = args.bound_address_file {
        let mut writer = File::create(bound_addr_path)?;
        writer.write_all(bound_addr.as_bytes())?;
        writer.write_all(b"\n")?;
    }

    // Monitoring is provided by the `Fb303Module`, but we must still start
    // stats aggregation.
    app.start_stats_aggregation()?;

    app.wait_until_terminated(
        move || will_exit.store(true, Ordering::Relaxed),
        args.shutdown_timeout_args.shutdown_grace_period,
        async {
            // Note that async blocks are lazy, so this isn't called until first poll
            let _ = task::spawn_blocking(move || {
                // Calling `stop` blocks until the service has completed all requests.
                service_framework.stop();
            })
            .await;
        },
        args.shutdown_timeout_args.shutdown_timeout,
    )?;
    info!(logger, "Exiting...");
    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 land_service_if::LandChangesetRequest;
use scuba_ext::MononokeScubaSampleBuilder;
use scuba_ext::ScubaValue;

use crate::conversion_helpers::convert_hex_to_str;

pub(crate) trait AddScubaRequest: Send + Sync {
    fn add_scuba_params(&self, _scuba: &mut MononokeScubaSampleBuilder) {}
}

impl AddScubaRequest for LandChangesetRequest {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
        scuba.add(
            "changesets",
            self.changesets
                .iter()
                .map(|changeset| convert_hex_to_str(changeset))
                .collect::<ScubaValue>(),
        );
        scuba.add("repo_name", self.repo_name.as_str());
        scuba.add("number_changesets", self.changesets.len());
    }
}


/*
 * 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 land_service_if::LandChangesetsResponse;
use scuba_ext::MononokeScubaSampleBuilder;

use crate::conversion_helpers::convert_hex_to_str;

/// A trait for logging part of a thrift `Response` struct to scuba.
pub(crate) trait AddScubaResponse: Send + Sync {
    fn add_scuba_response(&self, _scuba: &mut MononokeScubaSampleBuilder) {}
}

impl AddScubaResponse for LandChangesetsResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "new_head",
            convert_hex_to_str(&self.pushrebase_outcome.head),
        );
        scuba.add(
            "pushrebase_distance",
            self.pushrebase_outcome.pushrebase_distance.to_string(),
        );
        scuba.add("retry_num", self.pushrebase_outcome.retry_num.to_string());
        if let Some(old_bookmark_value) = &self.pushrebase_outcome.old_bookmark_value {
            scuba.add("old_bookmark_value", convert_hex_to_str(old_bookmark_value));
        }
    }
}