diff --git a/eden/mononoke/hooks/src/rust_hooks/mod.rs b/eden/mononoke/hooks/src/rust_hooks/mod.rs index 7fb091506b..ac74bec490 100644 --- a/eden/mononoke/hooks/src/rust_hooks/mod.rs +++ b/eden/mononoke/hooks/src/rust_hooks/mod.rs @@ -20,6 +20,7 @@ mod lua_pattern; 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; @@ -91,6 +92,11 @@ pub fn hook_name_to_file_hook( .set_from_config(config) .build()?, )), + "no_windows_filenames" => Some(Box::new( + no_windows_filenames::NoWindowsFilenames::builder() + .set_from_config(config) + .build()?, + )), _ => None, }) } diff --git a/eden/mononoke/hooks/src/rust_hooks/no_windows_filenames.rs b/eden/mononoke/hooks/src/rust_hooks/no_windows_filenames.rs new file mode 100644 index 0000000000..dfe8ced15b --- /dev/null +++ b/eden/mononoke/hooks/src/rust_hooks/no_windows_filenames.rs @@ -0,0 +1,103 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2. + */ + +use crate::{CrossRepoPushSource, FileContentFetcher, FileHook, HookExecution, HookRejectionInfo}; + +use anyhow::{Context, Result}; +use async_trait::async_trait; +use context::CoreContext; +use metaconfig_types::HookConfig; +use mononoke_types::{FileChange, MPath}; +use regex::bytes::Regex; + +#[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 { + Ok(NoWindowsFilenames { + allowed_paths: self + .allowed_paths + .map(Regex::new) + .transpose() + .context("Failed to create allowed_paths regex")?, + bad_windows_path_element: Regex::new(r"^(?i)(((com|lpt)\d$)|(con|prn|aux|nul))($|\.)")?, + }) + } +} + +pub struct NoWindowsFilenames { + allowed_paths: Option, + bad_windows_path_element: 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." +impl NoWindowsFilenames { + pub fn builder<'a>() -> NoWindowsFilenamesBuilder<'a> { + NoWindowsFilenamesBuilder::default() + } +} + +#[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 FileContentFetcher, + change: Option<&'change FileChange>, + path: &'path MPath, + cross_repo_push_source: CrossRepoPushSource, + ) -> Result { + 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); + } + } + + for element in path { + if self.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) + } +} diff --git a/eden/mononoke/tests/integration/test-hook-no-windows-filenames.t b/eden/mononoke/tests/integration/test-hook-no-windows-filenames.t new file mode 100644 index 0000000000..433267493e --- /dev/null +++ b/eden/mononoke/tests/integration/test-hook-no-windows-filenames.t @@ -0,0 +1,136 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License found in the LICENSE file in the root +# directory of this source tree. + + $ . "${TEST_FIXTURES}/library.sh" + $ export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 + + $ hook_test_setup no_windows_filenames <( \ + > echo 'bypass_pushvar="ALLOW_BAD_WINDOWS_FILENAMES=true"' + > ) + + $ hg up -q 0 + $ echo "ok" > "com" + $ hg ci -Aqm success + $ hgmn push -r . --to master_bookmark + pushing rev 2bdf0e02c487 to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + updating bookmark master_bookmark + + $ hg up -q 0 + $ echo "bad" > "COM5" + $ hg ci -Aqm failure + warning: filename contains 'COM5', which is reserved on Windows: COM5 + $ hgmn push -r . --to master_bookmark + pushing rev 0a31cb8056d1 to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + remote: Command failed + remote: Error: + remote: hooks failed: + remote: no_windows_filenames for 0a31cb8056d10d69d6652e754aeee9ecdd5f9e7b: ABORT: Illegal windows filename: COM5 + remote: + remote: Root cause: + remote: hooks failed: + remote: no_windows_filenames for 0a31cb8056d10d69d6652e754aeee9ecdd5f9e7b: ABORT: Illegal windows filename: COM5 + remote: + remote: Debug context: + remote: "hooks failed:\nno_windows_filenames for 0a31cb8056d10d69d6652e754aeee9ecdd5f9e7b: ABORT: Illegal windows filename: COM5" + abort: stream ended unexpectedly (got 0 bytes, expected 4) + [255] + + $ hg up -q 0 + $ echo "bad" > "nul.txt" + $ hg ci -Aqm failure + warning: filename contains 'nul', which is reserved on Windows: nul.txt + $ hgmn push -r . --to master_bookmark + pushing rev 7e7f8fb54a0b to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + remote: Command failed + remote: Error: + remote: hooks failed: + remote: no_windows_filenames for 7e7f8fb54a0b8f692fbf224a33476b864f11dfe9: ABORT: Illegal windows filename: nul.txt + remote: + remote: Root cause: + remote: hooks failed: + remote: no_windows_filenames for 7e7f8fb54a0b8f692fbf224a33476b864f11dfe9: ABORT: Illegal windows filename: nul.txt + remote: + remote: Debug context: + remote: "hooks failed:\nno_windows_filenames for 7e7f8fb54a0b8f692fbf224a33476b864f11dfe9: ABORT: Illegal windows filename: nul.txt" + abort: stream ended unexpectedly (got 0 bytes, expected 4) + [255] + + $ hg up -q 0 + $ mkdir dir + $ echo "bad" > dir/CoN.txt + $ hg ci -Aqm failure + warning: filename contains 'CoN', which is reserved on Windows: dir/CoN.txt + $ hgmn push -r . --to master_bookmark + pushing rev 49604693a23c to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + remote: Command failed + remote: Error: + remote: hooks failed: + remote: no_windows_filenames for 49604693a23c85a9ee0f6036d330b535842610dc: ABORT: Illegal windows filename: CoN.txt + remote: + remote: Root cause: + remote: hooks failed: + remote: no_windows_filenames for 49604693a23c85a9ee0f6036d330b535842610dc: ABORT: Illegal windows filename: CoN.txt + remote: + remote: Debug context: + remote: "hooks failed:\nno_windows_filenames for 49604693a23c85a9ee0f6036d330b535842610dc: ABORT: Illegal windows filename: CoN.txt" + abort: stream ended unexpectedly (got 0 bytes, expected 4) + [255] + + $ hg up -q 0 + $ mkdir dir + $ echo "ok" > dir/Icon.txt + $ hg ci -Aqm success + $ hgmn push -r . --to master_bookmark + pushing rev 74f01fef9e70 to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + updating bookmark master_bookmark + + $ hg up -q 0 + $ mkdir dir + $ echo "ok" > dir/Icom5 + $ hg ci -Aqm success + $ hgmn push -r . --to master_bookmark + pushing rev 47222c857e63 to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + updating bookmark master_bookmark + + $ hg up -q 0 + $ mkdir con + $ echo "bad" > con/foo + $ hg ci -Aqm failure + warning: filename contains 'con', which is reserved on Windows: con/foo + $ hgmn push -r . --to master_bookmark + pushing rev 115c8cee8249 to destination ssh://user@dummy/repo bookmark master_bookmark + searching for changes + remote: Command failed + remote: Error: + remote: hooks failed: + remote: no_windows_filenames for 115c8cee824903baec5607a5b5d731f4a92e5859: ABORT: Illegal windows filename: con + remote: + remote: Root cause: + remote: hooks failed: + remote: no_windows_filenames for 115c8cee824903baec5607a5b5d731f4a92e5859: ABORT: Illegal windows filename: con + remote: + remote: Debug context: + remote: "hooks failed:\nno_windows_filenames for 115c8cee824903baec5607a5b5d731f4a92e5859: ABORT: Illegal windows filename: con" + abort: stream ended unexpectedly (got 0 bytes, expected 4) + [255]