case-conflict checking functions

Summary:
Adds case conflict checking functions
- `manifest + path` case
- `[path]` case

Reviewed By: StanislavGlebik

Differential Revision: D9700760

fbshipit-source-id: 582430f61bed1ae279dafbe7804a562d5b2ddf59
This commit is contained in:
Pavel Aslanov 2018-09-07 09:04:20 -07:00 committed by Facebook Github Bot
parent c4ece89763
commit af69be4b3b
3 changed files with 174 additions and 2 deletions

View File

@ -15,7 +15,7 @@ use bytes::Bytes;
use db::{get_connection_params, InstanceRequirement, ProxyRequirement};
use failure::{Error, FutureFailureErrorExt, FutureFailureExt, Result, prelude::*};
use futures::{Async, IntoFuture, Poll};
use futures::future::{self, Either, Future};
use futures::future::{self, loop_fn, Either, Future, Loop};
use futures::stream::{self, Stream};
use futures::sync::oneshot;
use futures_ext::{BoxFuture, BoxStream, FutureExt, StreamExt};
@ -754,6 +754,60 @@ impl BlobRepo {
}
}
/// Check if adding path to manifest would cause case-conflict
///
/// Implementation traverses manifest and checks if correspoinding path element is present,
/// if path element is not present, it lowercases current path element and checks if it
/// collides with any existing elements inside manifest, if so it is a case-conflict.
pub fn check_case_conflict_in_manifest(
&self,
mf_id: &HgManifestId,
path: MPath,
) -> impl Future<Item = bool, Error = Error> {
self.get_manifest_by_nodeid(&mf_id.into_nodehash())
.and_then(|mf| {
loop_fn((mf, path.into_iter()), move |(mf, mut elements)| {
match elements.next() {
None => future::ok(Loop::Break(false)).left_future(),
Some(element) => {
match mf.lookup(&element) {
Some(entry) => {
// avoid fetching file content
match entry.get_type() {
Type::File(_) => {
future::ok(Loop::Break(false)).left_future()
}
Type::Tree => entry
.get_content()
.map(move |content| match content {
Content::Tree(mf) => Loop::Continue((mf, elements)),
_ => Loop::Break(false),
})
.right_future(),
}
}
None => {
let element_utf8 = String::from_utf8(element.to_bytes());
let has_case_conflict = mf.list().any(|entry| {
let name = entry
.get_name()
.expect("Non-root entry has empty name");
match (&element_utf8, String::from_utf8(name.to_bytes())) {
(Ok(ref element), Ok(ref name)) => {
name.to_lowercase() == element.to_lowercase()
}
_ => false,
}
});
future::ok(Loop::Break(has_case_conflict)).left_future()
}
}
}
}
})
})
}
pub fn find_path_in_manifest(
&self,
path: Option<MPath>,

View File

@ -752,3 +752,34 @@ fn test_get_manifest_from_bonsai() {
}
});
}
#[test]
fn test_case_conflict_in_manifest() {
async_unit::tokio_unit_test(|| {
let repo = many_files_dirs::getrepo(None);
let get_manifest_for_changeset = |cs_nodehash: &str| -> HgManifestId {
*run_future(repo.get_changeset_by_changesetid(&HgChangesetId::new(
string_to_nodehash(cs_nodehash),
))).unwrap()
.manifestid()
};
let mf = get_manifest_for_changeset("2f866e7e549760934e31bf0420a873f65100ad63");
for (path, result) in &[
("dir1/file_1_in_dir1", false),
("dir1/file_1_IN_dir1", true),
("DiR1/file_1_in_dir1", true),
("dir1/other_dir/file", false),
] {
assert_eq!(
run_future(repo.check_case_conflict_in_manifest(&mf, MPath::new(path).unwrap()))
.unwrap(),
*result,
"{} expected to {} cause conflict",
path,
if *result { "" } else { "not" },
);
}
});
}

View File

@ -5,10 +5,11 @@
// GNU General Public License version 2 or any later version.
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::convert::{From, TryFrom, TryInto};
use std::fmt::{self, Display};
use std::io::{self, Write};
use std::iter::{once, Once};
use std::iter::{once, FromIterator, Once};
use std::mem;
use std::slice::Iter;
@ -701,6 +702,69 @@ impl fmt::Debug for MPath {
}
}
struct CaseConflictTrie {
children: HashMap<MPathElement, CaseConflictTrie>,
lowercase: HashSet<String>,
}
impl CaseConflictTrie {
fn new() -> CaseConflictTrie {
CaseConflictTrie {
children: HashMap::new(),
lowercase: HashSet::new(),
}
}
/// Returns `true` if element was added successfully, or `false`
/// if trie already contains case conflicting entry.
fn add<P: IntoIterator<Item = MPathElement>>(&mut self, path: P) -> bool {
let mut iter = path.into_iter();
match iter.next() {
None => true,
Some(element) => {
if let Some(child) = self.children.get_mut(&element) {
return child.add(iter);
}
if let Ok(ref element) = String::from_utf8(element.to_bytes()) {
let element_lower = element.to_lowercase();
if self.lowercase.contains(&element_lower) {
return false;
} else {
self.lowercase.insert(element_lower);
}
}
self.children
.entry(element)
.or_insert(CaseConflictTrie::new())
.add(iter)
}
}
}
}
impl FromIterator<MPath> for CaseConflictTrie {
fn from_iter<I: IntoIterator<Item = MPath>>(iter: I) -> Self {
let mut trie = CaseConflictTrie::new();
for mpath in iter {
trie.add(mpath);
}
trie
}
}
/// Returns first path that would introduce a case-conflict, if any
pub fn check_case_conflicts<I: IntoIterator<Item = MPath>>(iter: I) -> Option<MPath> {
let mut trie = CaseConflictTrie::new();
for path in iter {
if !trie.add(path.clone()) {
return Some(path);
}
}
None
}
#[cfg(test)]
mod test {
use quickcheck::TestResult;
@ -878,6 +942,29 @@ mod test {
]).expect_err("unexpected OK - other paths and prefixes");
}
#[test]
fn case_conflicts() {
let mut trie: CaseConflictTrie = vec!["a/b/c", "a/d", "c/d/a"]
.into_iter()
.map(|p| MPath::new(p).unwrap())
.collect();
assert!(trie.add(MPath::new("a/b/c").unwrap()));
assert!(trie.add(MPath::new("a/B/d").unwrap()) == false);
assert!(trie.add(MPath::new("a/b/C").unwrap()) == false);
assert_eq!(
check_case_conflicts(vec![
MPath::new("a/b/c").unwrap(),
MPath::new("a/b/c").unwrap(), // not a case conflict
MPath::new("a/d").unwrap(),
MPath::new("a/B/d").unwrap(),
MPath::new("a/c").unwrap(),
]),
Some(MPath::new("a/B/d").unwrap()),
);
}
fn check_pcf_paths<I, T>(paths: I) -> Result<()>
where
I: IntoIterator<Item = (T, bool)>,