From 3198ff3b825798a8bcfbde0f893c9767b8cb5119 Mon Sep 17 00:00:00 2001 From: mcharsley Date: Wed, 8 Mar 2017 14:39:37 +0000 Subject: [PATCH] Initial dump of my code (#1) * Initial checkin. Supports -name, -name -print and a subset of -type Has unit tests, but no system tests * Corrected usage text * Added ability to replace stdout with a fake. Moved and_matcher to logical_matchers * Significant refactoring to allow tests for the arg-parsing code * Added tests for the argument parsing code. * Added support for or operator * Added change missing form last commit * Added AUTHORS and (MIT) LICENSE * Added support for lists (i.e. the comma operand) * Added support for parentheses * Cleaned up names of some Or/And/List matcher methods * Added support for depth-first searching * Added initial support for max_depth, min_depth and depth arguments. * Cleaned up a surfeit of Box::new's and change the [Or|And|List]Matchers to use builders, so submatchers can't be added later on. * Added support for -a * Added support for prune * Added prune * Refactored the way we fake stdoutput. Will allowing for easy expansion for stderr, clocks etc. * Renamed side effects to matcherio * Removed leftover tracing println call * Tidied up use statements * Switched to using walkdir * Added support for -newer * renamed new_sideeffetcs to new_matcher_io * Implemented ctime, atime and mtime * merged name_matcher and caseless_name_matcher into one module * Added support for -size * Minor tweaks based on code review * Switched Config struct to implement Default * Replaced try! with ? * Added copyright headers --- .gitignore | 2 +- AUTHORS | 4 + Cargo.lock | 162 ++++++ Cargo.toml | 17 + LICENSE | 2 +- src/find/main.rs | 15 + src/find/matchers/logical_matchers.rs | 481 +++++++++++++++++ src/find/matchers/mod.rs | 731 ++++++++++++++++++++++++++ src/find/matchers/name.rs | 139 +++++ src/find/matchers/printer.rs | 55 ++ src/find/matchers/prune.rs | 54 ++ src/find/matchers/size.rs | 173 ++++++ src/find/matchers/time.rs | 332 ++++++++++++ src/find/matchers/type_matcher.rs | 81 +++ src/find/mod.rs | 502 ++++++++++++++++++ src/lib.rs | 14 + test_data/depth/1/2/3/f3 | 0 test_data/depth/1/2/f2 | 0 test_data/depth/1/f1 | 0 test_data/depth/f0 | 0 test_data/simple/abbbc | 0 test_data/simple/subdir/ABBBC | 0 test_data/size/512bytes | Bin 0 -> 512 bytes 23 files changed, 2762 insertions(+), 2 deletions(-) create mode 100644 AUTHORS create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/find/main.rs create mode 100644 src/find/matchers/logical_matchers.rs create mode 100644 src/find/matchers/mod.rs create mode 100644 src/find/matchers/name.rs create mode 100644 src/find/matchers/printer.rs create mode 100644 src/find/matchers/prune.rs create mode 100644 src/find/matchers/size.rs create mode 100644 src/find/matchers/time.rs create mode 100644 src/find/matchers/type_matcher.rs create mode 100644 src/find/mod.rs create mode 100644 src/lib.rs create mode 100644 test_data/depth/1/2/3/f3 create mode 100644 test_data/depth/1/2/f2 create mode 100644 test_data/depth/1/f1 create mode 100644 test_data/depth/f0 create mode 100644 test_data/simple/abbbc create mode 100644 test_data/simple/subdir/ABBBC create mode 100644 test_data/size/512bytes diff --git a/.gitignore b/.gitignore index a5750a6..4536c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ target -Cargo.lock .gitignore .project .cargo +.settings diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..cccf6c2 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +# Names should be added to this file like so: +# # Name or Organization +# +# Google Inc. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..28be2ac --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,162 @@ +[root] +name = "findutils" +version = "0.1.0" +dependencies = [ + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 1.0.7 (git+https://github.com/mcharsley/walkdir?rev=dffefcf8db97a331a0f81d120e8aa20c1b36251e)", +] + +[[package]] +name = "aho-corasick" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "same-file" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tempdir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread-id" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unreachable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "walkdir" +version = "1.0.7" +source = "git+https://github.com/mcharsley/walkdir?rev=dffefcf8db97a331a0f81d120e8aa20c1b36251e#dffefcf8db97a331a0f81d120e8aa20c1b36251e" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0638fd549427caa90c499814196d1b9e3725eb4d15d7339d6de073a680ed0ca2" +"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)" = "684f330624d8c3784fb9558ca46c4ce488073a8d22450415c5eb4f4cfb0d11b5" +"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" +"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" +"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01" +"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457" +"checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" +"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" +"checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" +"checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7" +"checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91" +"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum walkdir 1.0.7 (git+https://github.com/mcharsley/walkdir?rev=dffefcf8db97a331a0f81d120e8aa20c1b36251e)" = "" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..450b5cf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "findutils" +version = "0.1.0" +authors = ["mcharsley "] + +[dependencies] +glob = "0.2" +# Current release of walkdir doesn't support depth-first operation. While we +# wait for https://github.com/BurntSushi/walkdir/pull/19 to be accepted and a +# new release made, let's access my branch directly. +walkdir = { git = "https://github.com/mcharsley/walkdir", rev = "dffefcf8db97a331a0f81d120e8aa20c1b36251e" } +tempdir = "0.3" +regex = "0.2" + +[[bin]] +name = "find" +path = "src/find/main.rs" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 0177c4a..a637ec5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) Jordi Boggiano +Copyright (c) Google Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/find/main.rs b/src/find/main.rs new file mode 100644 index 0000000..17a1ea0 --- /dev/null +++ b/src/find/main.rs @@ -0,0 +1,15 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +extern crate glob; +extern crate findutils; + +fn main() { + let args = std::env::args().collect::>(); + let strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect(); + let deps = findutils::find::StandardDependencies::new(); + std::process::exit(findutils::find::find_main(&strs, &deps)); +} diff --git a/src/find/matchers/logical_matchers.rs b/src/find/matchers/logical_matchers.rs new file mode 100644 index 0000000..6f04b34 --- /dev/null +++ b/src/find/matchers/logical_matchers.rs @@ -0,0 +1,481 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +//! This modules contains the matchers used for combining other matchers and +//! performing boolean logic on them (and a couple of trivial always-true and +//! always-false matchers). The design is strongly tied to the precedence rules +//! when parsing command-line options (e.g. "-foo -o -bar -baz" is equivalent +//! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz"). +use std::error::Error; +use std::iter::Iterator; +use walkdir::DirEntry; + +use find::matchers::{Matcher, MatcherIO}; + +/// This matcher contains a collection of other matchers. A file only matches +/// if it matches ALL the contained sub-matchers. For sub-matchers that have +/// side effects, the side effects occur in the same order as the sub-matchers +/// were pushed into the collection. +pub struct AndMatcher { + submatchers: Vec>, +} + +impl AndMatcher { + pub fn new(submatchers: Vec>) -> AndMatcher { + AndMatcher { submatchers: submatchers } + } +} + + +impl Matcher for AndMatcher { + /// Returns true if all sub-matchers return true. Short-circuiting does take + /// place. If the nth sub-matcher returns false, then we immediately return + /// and don't make any further calls. + fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + self.submatchers.iter().all(|ref x| x.matches(dir_entry, matcher_io)) + } + + fn has_side_effects(&self) -> bool { + self.submatchers.iter().any(|ref x| x.has_side_effects()) + } +} + +pub struct AndMatcherBuilder { + submatchers: Vec>, +} + +impl AndMatcherBuilder { + pub fn new() -> AndMatcherBuilder { + AndMatcherBuilder { submatchers: Vec::new() } + } + + pub fn new_and_condition(&mut self, matcher: Box) { + self.submatchers.push(matcher); + } + + /// Builds a Matcher: consuming the builder in the process. + pub fn build(mut self) -> Box { + // special case. If there's only one submatcher, just return that directly + if self.submatchers.len() == 1 { + // safe to unwrap: we've just checked the size + return self.submatchers.pop().unwrap(); + } + let matcher = Box::new(AndMatcher::new(self.submatchers)); + self.submatchers = Vec::new(); + matcher + } +} + + + +/// This matcher contains a collection of other matchers. A file matches +/// if it matches any of the contained sub-matchers. For sub-matchers that have +/// side effects, the side effects occur in the same order as the sub-matchers +/// were pushed into the collection. +pub struct OrMatcher { + submatchers: Vec>, +} + +impl OrMatcher { + pub fn new(submatchers: Vec>) -> OrMatcher { + OrMatcher { submatchers: submatchers } + } +} + + +impl Matcher for OrMatcher { + /// Returns true if any sub-matcher returns true. Short-circuiting does take + /// place. If the nth sub-matcher returns true, then we immediately return + /// and don't make any further calls. + fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + self.submatchers.iter().any(|ref x| x.matches(dir_entry, matcher_io)) + } + + fn has_side_effects(&self) -> bool { + self.submatchers.iter().any(|ref x| x.has_side_effects()) + } +} + +pub struct OrMatcherBuilder { + submatchers: Vec, +} + +impl OrMatcherBuilder { + pub fn new_and_condition(&mut self, matcher: Box) { + // safe to unwrap. submatchers always has at least one member + self.submatchers.last_mut().unwrap().new_and_condition(matcher); + } + + pub fn new_or_condition(&mut self, arg: &str) -> Result<(), Box> { + if self.submatchers.last().unwrap().submatchers.is_empty() { + return Err(From::from(format!("invalid expression; you have used a binary operator \ + '{}' with nothing before it.", + arg))); + } + self.submatchers.push(AndMatcherBuilder::new()); + Ok(()) + } + + pub fn new() -> OrMatcherBuilder { + let mut o = OrMatcherBuilder { submatchers: Vec::new() }; + o.submatchers.push(AndMatcherBuilder::new()); + o + } + + /// Builds a Matcher: consuming the builder in the process. + pub fn build(mut self) -> Box { + // Special case: if there's only one submatcher, just return that directly + if self.submatchers.len() == 1 { + // safe to unwrap: we've just checked the size + return self.submatchers.pop().unwrap().build(); + } + let mut submatchers = vec![]; + for x in self.submatchers { + submatchers.push(x.build()); + } + Box::new(OrMatcher::new(submatchers)) + } +} + + +/// This matcher contains a collection of other matchers. In contrast to +/// OrMatcher and AndMatcher, all the submatcher objects are called regardless +/// of the results of previous submatchers. This is primarily used for +/// submatchers with side-effects. For such sub-matchers the side effects occur +/// in the same order as the sub-matchers were pushed into the collection. +pub struct ListMatcher { + submatchers: Vec>, +} + +impl ListMatcher { + pub fn new(submatchers: Vec>) -> ListMatcher { + ListMatcher { submatchers: submatchers } + } +} + + +impl Matcher for ListMatcher { + /// Calls matches on all submatcher objects, with no short-circuiting. + /// Returns the result of the call to the final submatcher + fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + let mut rc = false; + for ref matcher in &self.submatchers { + rc = matcher.matches(dir_entry, matcher_io); + } + rc + } + + fn has_side_effects(&self) -> bool { + self.submatchers.iter().any(|ref x| x.has_side_effects()) + } +} + +pub struct ListMatcherBuilder { + submatchers: Vec, +} + +impl ListMatcherBuilder { + pub fn new_and_condition(&mut self, matcher: Box) { + // safe to unwrap. submatchers always has at least one member + self.submatchers.last_mut().unwrap().new_and_condition(matcher); + } + + pub fn new_or_condition(&mut self, arg: &str) -> Result<(), Box> { + self.submatchers.last_mut().unwrap().new_or_condition(arg) + } + + pub fn check_new_and_condition(&mut self) -> Result<(), Box> { + { + let child_or_matcher = &self.submatchers.last().unwrap(); + let grandchild_and_matcher = &child_or_matcher.submatchers.last().unwrap(); + + if grandchild_and_matcher.submatchers.is_empty() { + return Err(From::from("invalid expression; you have used a binary operator '-a' \ + with nothing before it.")); + } + } + Ok(()) + } + + pub fn new_list_condition(&mut self) -> Result<(), Box> { + { + let child_or_matcher = &self.submatchers.last().unwrap(); + let grandchild_and_matcher = &child_or_matcher.submatchers.last().unwrap(); + + if grandchild_and_matcher.submatchers.is_empty() { + return Err(From::from("invalid expression; you have used a binary operator ',' \ + with nothing before it.")); + } + } + self.submatchers.push(OrMatcherBuilder::new()); + Ok(()) + } + + pub fn new() -> ListMatcherBuilder { + let mut o = ListMatcherBuilder { submatchers: Vec::new() }; + o.submatchers.push(OrMatcherBuilder::new()); + o + } + + /// Builds a Matcher: consuming the builder in the process. + pub fn build(mut self) -> Box { + // Special case: if there's only one submatcher, just return that directly + if self.submatchers.len() == 1 { + // safe to unwrap: we've just checked the size + return self.submatchers.pop().unwrap().build(); + } + let mut submatchers = vec![]; + for x in self.submatchers { + submatchers.push(x.build()); + } + Box::new(ListMatcher::new(submatchers)) + } +} + + +/// A simple matcher that always matches. +pub struct TrueMatcher; + +impl TrueMatcher { + pub fn new_box() -> Box { + Box::new(TrueMatcher {}) + } +} + +impl Matcher for TrueMatcher { + fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { + true + } + + fn has_side_effects(&self) -> bool { + false + } +} + +/// A simple matcher that never matches. +pub struct FalseMatcher; + +impl Matcher for FalseMatcher { + fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { + false + } + + fn has_side_effects(&self) -> bool { + false + } +} + +impl FalseMatcher { + pub fn new_box() -> Box { + Box::new(FalseMatcher {}) + } +} + + +/// Matcher that wraps another matcher and inverts matching criteria. +pub struct NotMatcher { + submatcher: Box, +} + +impl NotMatcher { + pub fn new(submatcher: Box) -> NotMatcher { + NotMatcher { submatcher: submatcher } + } + + pub fn new_box(submatcher: Box) -> Box { + Box::new(NotMatcher::new(submatcher)) + } +} + +impl Matcher for NotMatcher { + fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + !self.submatcher.matches(dir_entry, matcher_io) + } + + fn has_side_effects(&self) -> bool { + self.submatcher.has_side_effects() + } +} + +#[cfg(test)] + +mod tests { + use walkdir::DirEntry; + use find::matchers::tests::get_dir_entry_for; + use super::*; + use find::matchers::{Matcher, MatcherIO}; + use find::tests::FakeDependencies; + + /// Simple Matcher impl that has side effects + pub struct HasSideEffects {} + + impl Matcher for HasSideEffects { + fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { + false + } + + fn has_side_effects(&self) -> bool { + true + } + } + + impl HasSideEffects { + pub fn new_box() -> Box { + Box::new(HasSideEffects {}) + } + } + + + + #[test] + fn and_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let mut builder = AndMatcherBuilder::new(); + let deps = FakeDependencies::new(); + + // start with one matcher returning true + builder.new_and_condition(TrueMatcher::new_box()); + assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + + builder = AndMatcherBuilder::new(); + builder.new_and_condition(TrueMatcher::new_box()); + builder.new_and_condition(FalseMatcher::new_box()); + assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn or_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let mut builder = OrMatcherBuilder::new(); + let deps = FakeDependencies::new(); + + // start with one matcher returning false + builder.new_and_condition(FalseMatcher::new_box()); + assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + + let mut builder = OrMatcherBuilder::new(); + builder.new_and_condition(FalseMatcher::new_box()); + builder.new_or_condition("-o").unwrap(); + builder.new_and_condition(TrueMatcher::new_box()); + assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn list_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let mut builder = ListMatcherBuilder::new(); + let deps = FakeDependencies::new(); + + // result should always match that of the last pushed submatcher + builder.new_and_condition(FalseMatcher::new_box()); + assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + + builder = ListMatcherBuilder::new(); + builder.new_and_condition(FalseMatcher::new_box()); + builder.new_list_condition().unwrap(); + builder.new_and_condition(TrueMatcher::new_box()); + assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + + builder = ListMatcherBuilder::new(); + builder.new_and_condition(FalseMatcher::new_box()); + builder.new_list_condition().unwrap(); + builder.new_and_condition(TrueMatcher::new_box()); + builder.new_list_condition().unwrap(); + builder.new_and_condition(FalseMatcher::new_box()); + assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn true_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = TrueMatcher {}; + let deps = FakeDependencies::new(); + + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn false_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = FalseMatcher {}; + let deps = FakeDependencies::new(); + + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn and_has_side_effects_works() { + let mut builder = AndMatcherBuilder::new(); + + // start with one matcher with no side effects false + builder.new_and_condition(TrueMatcher::new_box()); + assert!(!builder.build().has_side_effects()); + + builder = AndMatcherBuilder::new(); + builder.new_and_condition(TrueMatcher::new_box()); + builder.new_and_condition(HasSideEffects::new_box()); + assert!(builder.build().has_side_effects()); + } + + #[test] + fn or_has_side_effects_works() { + let mut builder = OrMatcherBuilder::new(); + + // start with one matcher with no side effects false + builder.new_and_condition(TrueMatcher::new_box()); + assert!(!builder.build().has_side_effects()); + + builder = OrMatcherBuilder::new(); + builder.new_and_condition(TrueMatcher::new_box()); + builder.new_and_condition(HasSideEffects::new_box()); + assert!(builder.build().has_side_effects()); + } + + #[test] + fn list_has_side_effects_works() { + let mut builder = ListMatcherBuilder::new(); + + // start with one matcher with no side effects false + builder.new_and_condition(TrueMatcher::new_box()); + assert!(!builder.build().has_side_effects()); + + builder = ListMatcherBuilder::new(); + builder.new_and_condition(TrueMatcher::new_box()); + builder.new_and_condition(HasSideEffects::new_box()); + assert!(builder.build().has_side_effects()); + } + + #[test] + fn true_has_side_effects_works() { + let matcher = TrueMatcher {}; + assert!(!matcher.has_side_effects()); + } + + #[test] + fn false_has_side_effects_works() { + let matcher = FalseMatcher {}; + assert!(!matcher.has_side_effects()); + } + + #[test] + fn not_matches_works() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let not_true = NotMatcher::new(TrueMatcher::new_box()); + let not_false = NotMatcher::new(FalseMatcher::new_box()); + let deps = FakeDependencies::new(); + assert!(!not_true.matches(&abbbc, &mut deps.new_matcher_io())); + assert!(not_false.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn not_has_side_effects_works() { + let has_fx = NotMatcher::new(HasSideEffects::new_box()); + let hasnt_fx = NotMatcher::new(FalseMatcher::new_box()); + assert!(has_fx.has_side_effects()); + assert!(!hasnt_fx.has_side_effects()); + } + +} diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs new file mode 100644 index 0000000..8a88b26 --- /dev/null +++ b/src/find/matchers/mod.rs @@ -0,0 +1,731 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +mod logical_matchers; +mod name; +mod printer; +mod prune; +mod size; +mod time; +mod type_matcher; + +use regex::Regex; +use std::error::Error; +use std::time::SystemTime; +use walkdir::DirEntry; + +use find::{Config, Dependencies}; + + + +/// Struct holding references to outputs and any inputs that can't be derived +/// from the file/directory info. +pub struct MatcherIO<'a> { + should_skip_dir: bool, + deps: &'a Dependencies<'a>, +} + +impl<'a> MatcherIO<'a> { + pub fn new(deps: &'a Dependencies<'a>) -> MatcherIO<'a> { + MatcherIO { + deps: deps, + should_skip_dir: false, + } + } + + pub fn mark_current_dir_to_be_skipped(&mut self) { + self.should_skip_dir = true; + } + + pub fn should_skip_current_dir(&self) -> bool { + self.should_skip_dir + } + + pub fn now(&self) -> SystemTime { + self.deps.now() + } +} + +/// A basic interface that can be used to determine whether a directory entry +/// is what's being searched for. To a first order approximation, find consists +/// of building a chain of Matcher objets, and then walking a directory tree, +/// passing each entry to the chain of Matchers. +pub trait Matcher { + /// Returns whether the given file matches the object's predicate. + fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool; + + /// Returns whether the matcher has any side-effects. Iff no such matcher + /// exists in the chain, then the filename will be printed to stdout. While + /// this is a compile-time fact for most matchers, it's run-time for matchers + /// that contain a collection of sub-Matchers. + fn has_side_effects(&self) -> bool; +} + +pub enum ComparableValue { + MoreThan(u64), + EqualTo(u64), + LessThan(u64), +} + +impl ComparableValue { + fn matches(&self, value: u64) -> bool { + match *self { + ComparableValue::MoreThan(limit) => value > limit, + ComparableValue::EqualTo(limit) => value == limit, + ComparableValue::LessThan(limit) => value < limit, + } + } + + /// same as matches, but takes a signed value + fn imatches(&self, value: i64) -> bool { + match *self { + ComparableValue::MoreThan(limit) => value >= 0 && (value as u64) > limit, + ComparableValue::EqualTo(limit) => value >= 0 && (value as u64) == limit, + ComparableValue::LessThan(limit) => value < 0 || (value as u64) < limit, + } + } +} + + +/// Builds a single AndMatcher containing the Matcher objects corresponding +/// to the passed in predicate arguments. +pub fn build_top_level_matcher(args: &[&str], + config: &mut Config) + -> Result, Box> { + let (_, top_level_matcher) = (build_matcher_tree(args, config, 0, false))?; + + // if the matcher doesn't have any side-effects, then we default to printing + if !top_level_matcher.has_side_effects() { + let mut new_and_matcher = logical_matchers::AndMatcherBuilder::new(); + new_and_matcher.new_and_condition(top_level_matcher); + new_and_matcher.new_and_condition(printer::Printer::new_box()); + return Ok(new_and_matcher.build()); + } + Ok(top_level_matcher) +} + +/// Helper function for build_matcher_tree +fn are_more_expressions(args: &[&str], index: usize) -> bool { + (index < args.len() - 1) && args[index + 1] != ")" +} + +fn convert_arg_to_number(option_name: &str, value_as_string: &str) -> Result> { + return match value_as_string.parse::() { + Ok(val) => Ok(val), + _ => { + Err(From::from(format!("Expected a positive decimal integer argument to {}, but got \ + `{}'", + option_name, + value_as_string))) + } + }; +} + +fn convert_arg_to_comparable_value(option_name: &str, + value_as_string: &str) + -> Result> { + let re = Regex::new(r"([+-]?)(\d+)$")?; + if let Some(groups) = re.captures(value_as_string) { + if let Ok(val) = groups[2].parse::() { + return Ok(match &groups[1] { + "+" => ComparableValue::MoreThan(val), + "-" => ComparableValue::LessThan(val), + _ => ComparableValue::EqualTo(val), + }); + } + } + Err(From::from(format!("Expected a decimal integer (with optional + or - prefix) argument \ + to {}, but got `{}'", + option_name, + value_as_string))) +} + +fn convert_arg_to_comparable_value_and_suffix(option_name: &str, + value_as_string: &str) + -> Result<(ComparableValue, String), Box> { + let re = Regex::new(r"([+-]?)(\d+)(.*)$")?; + if let Some(groups) = re.captures(value_as_string) { + if let Ok(val) = groups[2].parse::() { + return Ok((match &groups[1] { + "+" => ComparableValue::MoreThan(val), + "-" => ComparableValue::LessThan(val), + _ => ComparableValue::EqualTo(val), + }, + groups[3].to_string())); + } + } + Err(From::from(format!("Expected a decimal integer (with optional + or - prefix) and \ + (optional suffix) argument to {}, but got `{}'", + option_name, + value_as_string))) +} + + +/// The main "translate command-line args into a matcher" function. Will call +/// itself recursively if it encounters an opening bracket. A successful return +/// consits of a tuple containing the new index into the args array to use (if +/// called recursively) and the resulting matcher. +fn build_matcher_tree(args: &[&str], + config: &mut Config, + arg_index: usize, + expecting_bracket: bool) + -> Result<(usize, Box), Box> { + let mut top_level_matcher = logical_matchers::ListMatcherBuilder::new(); + + // can't use getopts for a variety or reasons: + // order of arguments is important + // arguments can start with + as well as - + // multiple-character flags don't start with a double dash + let mut i = arg_index; + let mut invert_next_matcher = false; + while i < args.len() { + let possible_submatcher = match args[i] { + "-print" => Some(printer::Printer::new_box()), + "-true" => Some(logical_matchers::TrueMatcher::new_box()), + "-false" => Some(logical_matchers::FalseMatcher::new_box()), + "-name" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(name::NameMatcher::new_box(args[i].as_ref())?) + } + "-iname" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(name::CaselessNameMatcher::new_box(args[i])?) + } + "-type" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(type_matcher::TypeMatcher::new_box(args[i])?) + } + "-newer" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(time::NewerMatcher::new_box(args[i])?) + } + "-mtime" | "-atime" | "-ctime" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + let file_time_type = match args[i] { + "-atime" => time::FileTimeType::Accessed, + "-ctime" => time::FileTimeType::Created, + "-mtime" => time::FileTimeType::Modified, + // This shouldn't be possible. We've already checked the value + // is one of those three values. + _ => unreachable!("Encountered unexpected value {}", args[i]), + }; + let days = convert_arg_to_comparable_value(args[i], args[i + 1])?; + i += 1; + Some(time::FileTimeMatcher::new_box(file_time_type, days)) + } + "-size" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + let (size, unit) = convert_arg_to_comparable_value_and_suffix(args[i], + args[i + 1])?; + i += 1; + Some(size::SizeMatcher::new_box(size, &unit)?) + } + + "-prune" => Some(prune::PruneMatcher::new_box()), + "-not" | "!" => { + if !are_more_expressions(args, i) { + return Err(From::from(format!("expected an expression after {}", args[i]))); + } + invert_next_matcher = true; + None + } + "-a" => { + if !are_more_expressions(args, i) { + return Err(From::from(format!("expected an expression after {}", args[i]))); + } + top_level_matcher.check_new_and_condition()?; + None + } + "-or" | "-o" => { + if !are_more_expressions(args, i) { + return Err(From::from(format!("expected an expression after {}", args[i]))); + } + top_level_matcher.new_or_condition(args[i])?; + None + } + "," => { + if !are_more_expressions(args, i) { + return Err(From::from(format!("expected an expression after {}", args[i]))); + } + top_level_matcher.new_list_condition()?; + None + } + "(" => { + let (new_arg_index, sub_matcher) = build_matcher_tree(args, config, i + 1, true)?; + i = new_arg_index; + Some(sub_matcher) + } + ")" => { + if !expecting_bracket { + return Err(From::from("you have too many ')'")); + } + return Ok((i, top_level_matcher.build())); + } + "-d" | "-depth" => { + // TODO add warning if it appears after actual testing criterion + config.depth_first = true; + None + } + "-sorted" => { + // TODO add warning if it appears after actual testing criterion + config.sorted_output = true; + None + } + "-maxdepth" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + config.max_depth = convert_arg_to_number(args[i], args[i + 1])?; + i += 1; + None + } + "-mindepth" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + config.min_depth = convert_arg_to_number(args[i], args[i + 1])?; + i += 1; + None + } + + _ => return Err(From::from(format!("Unrecognized flag: '{}'", args[i]))), + }; + if let Some(submatcher) = possible_submatcher { + if invert_next_matcher { + top_level_matcher.new_and_condition(logical_matchers::NotMatcher::new_box(submatcher)); + invert_next_matcher = false; + } else { + top_level_matcher.new_and_condition(submatcher); + } + } + i += 1; + } + if expecting_bracket { + return Err(From::from("invalid expression; I was expecting to find a ')' somewhere but \ + did not see one.")); + } + Ok((i, top_level_matcher.build())) +} + +#[cfg(test)] +mod tests { + use walkdir::{DirEntry, WalkDir}; + use find::Config; + use find::tests::FakeDependencies; + use super::*; + + + + /// Helper function for tests to get a DirEntry object. directory should + /// probably be a string starting with "test_data/" (cargo's tests run with + /// a working directory set to the root findutils folder). + pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { + for wrapped_dir_entry in WalkDir::new(directory) { + let dir_entry = wrapped_dir_entry.unwrap(); + if dir_entry.file_name().to_string_lossy() == filename { + return dir_entry; + } + } + panic!("Couldn't find {} in {}", directory, filename); + } + + #[test] + fn build_top_level_matcher_name() { + let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); + let abbbc_upper = get_dir_entry_for("./test_data/simple/subdir", "ABBBC"); + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&["-name", "a*c"], &mut config).unwrap(); + + assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); + assert!(!matcher.matches(&abbbc_upper, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); + } + + #[test] + fn build_top_level_matcher_iname() { + let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); + let abbbc_upper = get_dir_entry_for("./test_data/simple/subdir", "ABBBC"); + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&["-iname", "a*c"], &mut config).unwrap(); + + assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); + assert!(matcher.matches(&abbbc_upper, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), + "./test_data/simple/abbbc\n./test_data/simple/subdir/ABBBC\n"); + } + + #[test] + fn build_top_level_matcher_not() { + for arg in &["-not", "!"] { + let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&[arg, "-name", "doesntexist"], &mut config) + .unwrap(); + + assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); + } + } + + #[test] + fn build_top_level_matcher_not_needs_expression() { + for arg in &["-not", "!"] { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&[arg], &mut config) { + assert!(e.description().contains("expected an expression")); + } else { + panic!("parsing arugment lists that end in -not should fail"); + } + } + } + + #[test] + fn build_top_level_matcher_missing_args() { + for arg in &["-iname", "-name", "-type"] { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&[arg], &mut config) { + assert!(e.description().contains("missing argument to")); + assert!(e.description().contains(arg)); + } else { + panic!("parsing arugment lists that end in -not should fail"); + } + } + } + + #[test] + fn build_top_level_matcher_or_without_expr1() { + for arg in &["-or", "-o"] { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&[arg, "-true"], &mut config) { + assert!(e.description().contains("you have used a binary operator")); + } else { + panic!("parsing arugment list that begins with -or should fail"); + } + } + } + + #[test] + fn build_top_level_matcher_or_without_expr2() { + for arg in &["-or", "-o"] { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-true", arg], &mut config) { + assert!(e.description().contains("expected an expression")); + } else { + panic!("parsing arugment list that ends with -or should fail"); + } + } + } + + #[test] + fn build_top_level_matcher_and_without_expr1() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-a", "-true"], &mut config) { + assert!(e.description().contains("you have used a binary operator")); + } else { + panic!("parsing arugment list that begins with -a should fail"); + } + } + + #[test] + fn build_top_level_matcher_and_without_expr2() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-true", "-a"], &mut config) { + assert!(e.description().contains("expected an expression")); + } else { + panic!("parsing arugment list that ends with -or should fail"); + } + } + + #[test] + fn build_top_level_matcher_dash_a_works() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + // build a matcher using an explicit -a argument + let matcher = build_top_level_matcher(&["-true", "-a", "-true"], &mut config).unwrap(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); + } + + #[test] + fn build_top_level_matcher_or_works() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + for args in &[["-true", "-o", "-false"], + ["-false", "-o", "-true"], + ["-true", "-o", "-true"]] { + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(args, &mut config).unwrap(); + + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); + } + + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&["-false", "-o", "-false"], &mut config).unwrap(); + + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), ""); + } + + #[test] + fn build_top_level_matcher_and_works() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + for args in &[["-true", "-false"], ["-false", "-true"], ["-false", "-false"]] { + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(args, &mut config).unwrap(); + + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), ""); + } + + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&["-true", "-true"], &mut config).unwrap(); + + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); + } + + #[test] + fn build_top_level_matcher_list_works() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + let args = ["-true", "-print", "-false", ",", "-print", "-false"]; + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + let matcher = build_top_level_matcher(&args, &mut config).unwrap(); + + // final matcher returns false, so list matcher should too + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + // two print matchers means doubled output + assert_eq!(deps.get_output_as_string(), + "./test_data/simple/abbbc\n./test_data/simple/abbbc\n"); + } + + #[test] + fn build_top_level_matcher_list_without_expr1() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&[",", "-true"], &mut config) { + assert!(e.description().contains("you have used a binary operator")); + } else { + panic!("parsing arugment list that begins with , should fail"); + } + + if let Err(e) = build_top_level_matcher(&["-true", "-o", ",", "-true"], &mut config) { + assert!(e.description().contains("you have used a binary operator")); + } else { + panic!("parsing arugment list that contains '-o ,' should fail"); + } + } + + #[test] + fn build_top_level_matcher_list_without_expr2() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-true", ","], &mut config) { + assert!(e.description().contains("expected an expression")); + } else { + panic!("parsing arugment list that ends with , should fail"); + } + } + + #[test] + fn build_top_level_matcher_not_enough_brackets() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-true", "("], &mut config) { + assert!(e.description().contains("I was expecting to find a ')'")); + } else { + panic!("parsing arugment list with not enough closing brackets should fail"); + } + } + + #[test] + fn build_top_level_matcher_too_many_brackets() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-true", "(", ")", ")"], &mut config) { + assert!(e.description().contains("too many ')'")); + } else { + panic!("parsing arugment list with too many closing brackets should fail"); + } + } + + #[test] + fn build_top_level_matcher_can_use_bracket_as_arg() { + let mut config = Config::default(); + // make sure that if we use a bracket as an argument (e.g. to -name) + // then it isn't viewed as a bracket + build_top_level_matcher(&["-name", "("], &mut config).unwrap(); + build_top_level_matcher(&["-name", ")"], &mut config).unwrap(); + } + + #[test] + fn build_top_level_matcher_brackets_work() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + // same as true | ( false & false) = true + let args_without = ["-true", "-o", "-false", "-false"]; + // same as (true | false) & false = false + let args_with = ["(", "-true", "-o", "-false", ")", "-false"]; + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + { + let matcher = build_top_level_matcher(&args_without, &mut config).unwrap(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + { + let matcher = build_top_level_matcher(&args_with, &mut config).unwrap(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + } + + #[test] + fn build_top_level_matcher_not_and_brackets_work() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + // same as (true & !(false)) | true = true + let args_without = ["-true", "-not", "-false", "-o", "-true"]; + // same as true & !(false | true) = false + let args_with = ["-true", "-not", "(", "-false", "-o", "-true", ")"]; + let mut config = Config::default(); + let deps = FakeDependencies::new(); + + { + let matcher = build_top_level_matcher(&args_without, &mut config).unwrap(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + { + let matcher = build_top_level_matcher(&args_with, &mut config).unwrap(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + } + + #[test] + fn comparable_value_matches() { + + assert!(!ComparableValue::LessThan(0).matches(0), + "0 should not be less than 0"); + assert!(ComparableValue::LessThan(u64::max_value()).matches(0), + "0 should be less than max_value"); + assert!(!ComparableValue::LessThan(0).matches(u64::max_value()), + "max_value should not be less than 0"); + assert!(!ComparableValue::LessThan(u64::max_value()).matches(u64::max_value()), + "max_value should not be less than max_value"); + + assert!(ComparableValue::EqualTo(0).matches(0), + "0 should be equal to 0"); + assert!(!ComparableValue::EqualTo(u64::max_value()).matches(0), + "0 should not be equal to max_value"); + assert!(!ComparableValue::EqualTo(0).matches(u64::max_value()), + "max_value should not be equal to 0"); + assert!(ComparableValue::EqualTo(u64::max_value()).matches(u64::max_value()), + "max_value should be equal to max_value"); + + assert!(!ComparableValue::MoreThan(0).matches(0), + "0 should not be more than 0"); + assert!(!ComparableValue::MoreThan(u64::max_value()).matches(0), + "0 should not be more than max_value"); + assert!(ComparableValue::MoreThan(0).matches(u64::max_value()), + "max_value should be more than 0"); + assert!(!ComparableValue::MoreThan(u64::max_value()).matches(u64::max_value()), + "max_value should not be more than max_value"); + } + + #[test] + fn comparable_value_imatches() { + + assert!(!ComparableValue::LessThan(0).imatches(0), + "0 should not be less than 0"); + assert!(ComparableValue::LessThan(u64::max_value()).imatches(0), + "0 should be less than max_value"); + assert!(!ComparableValue::LessThan(0).imatches(i64::max_value()), + "max_value should not be less than 0"); + assert!(ComparableValue::LessThan(u64::max_value()).imatches(i64::max_value()), + "max_value should be less than max_value"); + assert!(ComparableValue::LessThan(0).imatches(i64::min_value()), + "min_value should be less than 0"); + assert!(ComparableValue::LessThan(u64::max_value()).imatches(i64::min_value()), + "min_value should be less than max_value"); + + assert!(ComparableValue::EqualTo(0).imatches(0), + "0 should be equal to 0"); + assert!(!ComparableValue::EqualTo(u64::max_value()).imatches(0), + "0 should not be equal to max_value"); + assert!(!ComparableValue::EqualTo(0).imatches(i64::max_value()), + "max_value should not be equal to 0"); + assert!(!ComparableValue::EqualTo(u64::max_value()).imatches(i64::max_value()), + "max_value should not be equal to i64::max_value"); + assert!(ComparableValue::EqualTo(i64::max_value() as u64).imatches(i64::max_value()), + "i64::max_value should be equal to i64::max_value"); + assert!(!ComparableValue::EqualTo(0).imatches(i64::min_value()), + "min_value should not be equal to 0"); + assert!(!ComparableValue::EqualTo(u64::max_value()).imatches(i64::min_value()), + "min_value should not be equal to max_value"); + + assert!(!ComparableValue::MoreThan(0).imatches(0), + "0 should not be more than 0"); + assert!(!ComparableValue::MoreThan(u64::max_value()).imatches(0), + "0 should not be more than max_value"); + assert!(ComparableValue::MoreThan(0).imatches(i64::max_value()), + "max_value should be more than 0"); + assert!(!ComparableValue::MoreThan(u64::max_value()).imatches(i64::max_value()), + "max_value should not be more than max_value"); + assert!(!ComparableValue::MoreThan(0).imatches(i64::min_value()), + "min_value should not be more than 0"); + assert!(!ComparableValue::MoreThan(u64::max_value()).imatches(i64::min_value()), + "min_value should not be more than max_value"); + } + + #[test] + fn build_top_level_matcher_bad_ctime_value() { + let mut config = Config::default(); + + if let Err(e) = build_top_level_matcher(&["-ctime", "-123."], &mut config) { + assert!(e.description().contains("Expected a decimal integer"), + "bad description: {}", + e); + } else { + panic!("parsing a bad ctime value should fail"); + } + } +} diff --git a/src/find/matchers/name.rs b/src/find/matchers/name.rs new file mode 100644 index 0000000..eba2f07 --- /dev/null +++ b/src/find/matchers/name.rs @@ -0,0 +1,139 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use glob::Pattern; +use glob::PatternError; +use walkdir::DirEntry; + +use find::matchers::{Matcher, MatcherIO}; + +/// This matcher makes a case-sensitive comparison of the name against a +/// shell wildcard pattern. See glob::Pattern for details on the exact +/// syntax. +pub struct NameMatcher { + pattern: Pattern, +} + +impl NameMatcher { + pub fn new(pattern_string: &str) -> Result { + let p = Pattern::new(pattern_string)?; + Ok(NameMatcher { pattern: p }) + } + + pub fn new_box(pattern_string: &str) -> Result, PatternError> { + Ok(Box::new(NameMatcher::new(pattern_string)?)) + } +} + +impl Matcher for NameMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + return self.pattern.matches(file_info.file_name().to_string_lossy().as_ref()); + } + + fn has_side_effects(&self) -> bool { + false + } +} + +/// This matcher makes a case-insensitive comparison of the name against a +/// shell wildcard pattern. See glob::Pattern for details on the exact +/// syntax. +pub struct CaselessNameMatcher { + pattern: Pattern, +} + +impl CaselessNameMatcher { + pub fn new(pattern_string: &str) -> Result { + let p = Pattern::new(&pattern_string.to_lowercase())?; + Ok(CaselessNameMatcher { pattern: p }) + } + + pub fn new_box(pattern_string: &str) -> Result, PatternError> { + Ok(Box::new(CaselessNameMatcher::new(pattern_string)?)) + } +} + +impl super::Matcher for CaselessNameMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + return self.pattern + .matches(file_info.file_name().to_string_lossy().to_lowercase().as_ref()); + } + + fn has_side_effects(&self) -> bool { + false + } +} + + +#[cfg(test)] + +mod tests { + use find::matchers::Matcher; + use find::matchers::tests::get_dir_entry_for; + use find::tests::FakeDependencies; + use super::*; + + + #[test] + fn matching_with_wrong_case_returns_false() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = NameMatcher::new(&"A*C".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn matching_with_right_case_returns_true() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = NameMatcher::new(&"abb?c".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn not_matching_returns_false() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = NameMatcher::new(&"should't match".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn cant_create_with_invalid_pattern() { + let result = NameMatcher::new(&"a**c".to_string()); + assert!(result.is_err()); + } + + #[test] + fn caseless_matching_with_wrong_case_returns_true() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = CaselessNameMatcher::new(&"A*C".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn caseless_matching_with_right_case_returns_true() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = CaselessNameMatcher::new(&"abb?c".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn caseless_not_matching_returns_false() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = CaselessNameMatcher::new(&"should't match".to_string()).unwrap(); + let deps = FakeDependencies::new(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn caseless_cant_create_with_invalid_pattern() { + let result = CaselessNameMatcher::new(&"a**c".to_string()); + assert!(result.is_err()); + } +} diff --git a/src/find/matchers/printer.rs b/src/find/matchers/printer.rs new file mode 100644 index 0000000..1a67d47 --- /dev/null +++ b/src/find/matchers/printer.rs @@ -0,0 +1,55 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use walkdir::DirEntry; + +use find::matchers::{Matcher, MatcherIO}; + +/// This matcher just prints the name of the file to stdout. +pub struct Printer; + +impl Printer { + pub fn new() -> Printer { + Printer {} + } + + pub fn new_box() -> Box { + Box::new(Printer::new()) + } +} + +impl Matcher for Printer { + fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + writeln!(matcher_io.deps.get_output().borrow_mut(), + "{}", + file_info.path().to_string_lossy()) + .unwrap(); + true + } + + fn has_side_effects(&self) -> bool { + true + } +} + +#[cfg(test)] + +mod tests { + use find::matchers::tests::get_dir_entry_for; + use find::matchers::Matcher; + use find::tests::FakeDependencies; + use super::*; + + #[test] + fn prints() { + let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); + + let matcher = Printer::new(); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + assert_eq!("./test_data/simple/abbbc\n", deps.get_output_as_string()); + } +} diff --git a/src/find/matchers/prune.rs b/src/find/matchers/prune.rs new file mode 100644 index 0000000..de2e8ab --- /dev/null +++ b/src/find/matchers/prune.rs @@ -0,0 +1,54 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use walkdir::DirEntry; + +use find::matchers::{Matcher, MatcherIO}; + +/// This matcher checks the type of the file. +pub struct PruneMatcher; + +impl PruneMatcher { + pub fn new() -> PruneMatcher { + PruneMatcher {} + } + + pub fn new_box() -> Box { + Box::new(PruneMatcher::new()) + } +} + +impl Matcher for PruneMatcher { + fn matches(&self, _: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + matcher_io.mark_current_dir_to_be_skipped(); + return true; + } + + fn has_side_effects(&self) -> bool { + false + } +} +#[cfg(test)] + +mod tests { + use find::matchers::Matcher; + use find::matchers::tests::get_dir_entry_for; + use find::tests::FakeDependencies; + use super::*; + + #[test] + fn file_type_matcher() { + let dir = get_dir_entry_for("test_data", "simple"); + let deps = FakeDependencies::new(); + + let mut matcher_io = deps.new_matcher_io(); + assert!(!matcher_io.should_skip_current_dir()); + let matcher = PruneMatcher::new(); + assert!(matcher.matches(&dir, &mut matcher_io)); + assert!(matcher_io.should_skip_current_dir()); + } + +} diff --git a/src/find/matchers/size.rs b/src/find/matchers/size.rs new file mode 100644 index 0000000..f6532af --- /dev/null +++ b/src/find/matchers/size.rs @@ -0,0 +1,173 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std::error::Error; +use std::io::{stderr, Write}; +use std::str::FromStr; +use walkdir::DirEntry; + +use find::matchers::{ComparableValue, Matcher, MatcherIO}; + +#[derive(Clone, Copy, Debug)] +enum Unit { + Byte, + TwoByteWord, + Block, + KibiByte, + MebiByte, + GibiByte, +} + +impl FromStr for Unit { + type Err = Box; + fn from_str(s: &str) -> Result> { + Ok(match s { + "c" => Unit::Byte, + "w" => Unit::TwoByteWord, + "" | "b" => Unit::Block, + "k" => Unit::KibiByte, + "M" => Unit::MebiByte, + "G" => Unit::GibiByte, + _ => { + return Err(From::from(format!("Invalid suffix {} for -size. Only allowed \ + values are , b, c, w, k, M or G", + s))); + } + }) + } +} + +fn byte_size_to_unit_size(unit: Unit, byte_size: u64) -> u64 { + // Short circuit (to avoid a overflow error when subtracting 1 later on) + if byte_size == 0 { + return 0; + } + let bits_to_shift = match unit { + Unit::Byte => 0, + Unit::TwoByteWord => 1, + Unit::Block => 9, + Unit::KibiByte => 10, + Unit::MebiByte => 20, + Unit::GibiByte => 30, + }; + // Skip pointless arithmetic. + if bits_to_shift == 0 { + return byte_size; + } + // We want to round up (e.g. 1 byte - 1024 bytes = 1k. + // 1025 bytes to 2048 bytes = 2k etc. + ((byte_size - 1) >> bits_to_shift) + 1 +} + +/// Matcher that checks whether a file's size if {less than | equal to | more than} +/// N units in size. +pub struct SizeMatcher { + value_to_match: ComparableValue, + unit: Unit, +} + +impl SizeMatcher { + pub fn new(value_to_match: ComparableValue, + suffix_string: &str) + -> Result> { + Ok(SizeMatcher { + unit: suffix_string.parse()?, + value_to_match: value_to_match, + }) + } + + pub fn new_box(value_to_match: ComparableValue, + suffix_string: &str) + -> Result, Box> { + Ok(Box::new(SizeMatcher::new(value_to_match, suffix_string)?)) + } +} + +impl Matcher for SizeMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + match file_info.metadata() { + Ok(metadata) => { + self.value_to_match + .matches(byte_size_to_unit_size(self.unit, metadata.len())) + } + Err(e) => { + writeln!(&mut stderr(), + "Error getting file size for {}: {}", + file_info.path().to_string_lossy(), + e) + .unwrap(); + false + } + } + } + + fn has_side_effects(&self) -> bool { + false + } +} + +#[cfg(test)] +mod tests { + use find::matchers::{ComparableValue, Matcher}; + use find::matchers::tests::get_dir_entry_for; + use find::tests::FakeDependencies; + use super::*; + // need to explicitly use non-pub members + use super::{byte_size_to_unit_size, Unit}; + + #[test] + fn test_byte_size_to_unit_size() { + assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 0), 0); + assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1), 1); + assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1024), 1); + assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1025), 2); + assert_eq!(byte_size_to_unit_size(Unit::Byte, 1025), 1025); + assert_eq!(byte_size_to_unit_size(Unit::TwoByteWord, 1025), 513); + assert_eq!(byte_size_to_unit_size(Unit::Block, 1025), 3); + assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1025), 2); + assert_eq!(byte_size_to_unit_size(Unit::MebiByte, 1024 * 1024 + 1), 2); + assert_eq!(byte_size_to_unit_size(Unit::GibiByte, 1024 * 1024 * 1024 + 1), + 2); + } + + #[test] + fn unit_from_string() { + assert_eq!(byte_size_to_unit_size("c".parse().unwrap(), 2), 2); + assert_eq!(byte_size_to_unit_size("w".parse().unwrap(), 3), 2); + assert_eq!(byte_size_to_unit_size("b".parse().unwrap(), 513), 2); + assert_eq!(byte_size_to_unit_size("".parse().unwrap(), 513), 2); + assert_eq!(byte_size_to_unit_size("k".parse().unwrap(), 1025), 2); + assert_eq!(byte_size_to_unit_size("M".parse().unwrap(), 1024 * 1024 + 1), + 2); + assert_eq!(byte_size_to_unit_size("G".parse().unwrap(), 2024 * 1024 * 1024 + 1), + 2); + } + + #[test] + fn size_matcher_bad_unit() { + if let Err(e) = SizeMatcher::new(ComparableValue::EqualTo(2), "xyz") { + assert!(e.description().contains("Invalid suffix") && e.description().contains("xyz"), + "bad description: {}", + e); + } else { + panic!("parsing a unit string should fail"); + } + } + + #[test] + fn size_matcher() { + let file_info = get_dir_entry_for("./test_data/size", "512bytes"); + + let equal_to_2_blocks = SizeMatcher::new(ComparableValue::EqualTo(2), "b").unwrap(); + let equal_to_1_blocks = SizeMatcher::new(ComparableValue::EqualTo(1), "b").unwrap(); + let deps = FakeDependencies::new(); + + assert!(!equal_to_2_blocks.matches(&file_info, &mut deps.new_matcher_io()), + "512-byte file should not match size of 2 blocks"); + assert!(equal_to_1_blocks.matches(&file_info, &mut deps.new_matcher_io()), + "512-byte file should match size of 1 block"); + } +} diff --git a/src/find/matchers/time.rs b/src/find/matchers/time.rs new file mode 100644 index 0000000..d0c8635 --- /dev/null +++ b/src/find/matchers/time.rs @@ -0,0 +1,332 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std; +use std::error::Error; +use std::fs::{File, Metadata}; +use std::io::{stderr, Write}; +use std::time::SystemTime; +use walkdir::DirEntry; + +use find::matchers::{ComparableValue, Matcher, MatcherIO}; + +const SECONDS_PER_DAY: i64 = 60 * 60 * 24; + +/// This matcher checks whether a file is newer than the file the matcher is initialized with. +pub struct NewerMatcher { + given_modification_time: SystemTime, +} + +impl NewerMatcher { + pub fn new(path_to_file: &str) -> Result> { + let f = File::open(path_to_file)?; + let metadata = f.metadata()?; + Ok(NewerMatcher { given_modification_time: metadata.modified()? }) + } + + pub fn new_box(path_to_file: &str) -> Result, Box> { + Ok(Box::new(NewerMatcher::new(path_to_file)?)) + } + + /// Impementation of matches that returns a result, allowing use to use try! + /// to deal with the errors. + fn matches_impl(&self, file_info: &DirEntry) -> Result> { + let this_time = file_info.metadata()?.modified()?; + // duration_since returns an Ok duration if this_time <= given_modification_time + // and returns an Err (with a duration) otherwise. So if this_time > + // given_modification_time (in which case we want to return true) then + // duration_since will return an error. + Ok(self.given_modification_time.duration_since(this_time).is_err()) + } +} + +impl Matcher for NewerMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + match self.matches_impl(file_info) { + Err(e) => { + writeln!(&mut stderr(), + "Error getting modification time for {}: {}", + file_info.path().to_string_lossy(), + e) + .unwrap(); + false + } + Ok(t) => t, + } + } + + fn has_side_effects(&self) -> bool { + false + } +} + +#[derive(Clone, Copy, Debug)] +pub enum FileTimeType { + Accessed, + Created, + Modified, +} + +impl FileTimeType { + fn get_file_time(self, metadata: Metadata) -> std::io::Result { + match self { + FileTimeType::Accessed => metadata.accessed(), + FileTimeType::Created => metadata.created(), + FileTimeType::Modified => metadata.modified(), + } + } +} + +/// This matcher checks whether a file's accessed|creation|modification time is +/// {less than | exactly | more than} N days old. +pub struct FileTimeMatcher { + days: ComparableValue, + file_time_type: FileTimeType, +} + +impl Matcher for FileTimeMatcher { + fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { + match self.matches_impl(file_info, matcher_io.now()) { + Err(e) => { + writeln!(&mut stderr(), + "Error getting {:?} time for {}: {}", + self.file_time_type, + file_info.path().to_string_lossy(), + e) + .unwrap(); + false + } + Ok(t) => t, + } + } + + fn has_side_effects(&self) -> bool { + false + } +} + + + +impl FileTimeMatcher { + /// Impementation of matches that returns a result, allowing use to use try! + /// to deal with the errors. + fn matches_impl(&self, file_info: &DirEntry, now: SystemTime) -> Result> { + let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?; + let mut is_negative = false; + // durations can't be negative. So duration_since returns a duration + // wrapped in an error if now < this_time. + let age = match now.duration_since(this_time) { + Ok(duration) => duration, + Err(e) => { + is_negative = true; + e.duration() + } + }; + let age_in_seconds: i64 = age.as_secs() as i64 * if is_negative { -1 } else { 1 }; + // rust division truncates towards zero (see + // https://github.com/rust-lang/rust/blob/master/src/libcore/ops.rs#L580 ) + // so a simple age_in_seconds / SECONDS_PER_DAY gives the wrong answer + // for negative ages: a file whose age is 1 second in the future needs to + // count as -1 day old, not 0. + let age_in_days = age_in_seconds / SECONDS_PER_DAY + if is_negative { -1 } else { 0 }; + Ok(self.days.imatches(age_in_days)) + } + + pub fn new(file_time_type: FileTimeType, days: ComparableValue) -> FileTimeMatcher { + FileTimeMatcher { + file_time_type: file_time_type, + days: days, + } + } + + pub fn new_box(file_time_type: FileTimeType, days: ComparableValue) -> Box { + Box::new(FileTimeMatcher::new(file_time_type, days)) + } +} + +#[cfg(test)] +mod tests { + use std::fs::{File, OpenOptions}; + use std::io::{Read, Write}; + use std::thread; + use std::time::{Duration, SystemTime}; + use tempdir::TempDir; + use walkdir::DirEntry; + + use find::matchers::{ComparableValue, Matcher}; + use find::matchers::tests::get_dir_entry_for; + use find::tests::FakeDependencies; + use super::*; + + #[test] + fn newer_matcher() { + // this file should already exist + let old_file = get_dir_entry_for("test_data", "simple"); + + let temp_dir = TempDir::new("newer_matcher").unwrap(); + let temp_dir_path = temp_dir.path().to_string_lossy(); + // this has just been created, so should be newer + let new_file_name = "newFile"; + File::create(temp_dir.path().join(new_file_name)).expect("create temp file"); + + let new_file = get_dir_entry_for(&temp_dir_path, &new_file_name); + + let matcher_for_new = + NewerMatcher::new(&temp_dir.path().join(new_file_name).to_string_lossy()).unwrap(); + let matcher_for_old = NewerMatcher::new(&old_file.path().to_string_lossy()).unwrap(); + let deps = FakeDependencies::new(); + + assert!(!matcher_for_new.matches(&old_file, &mut deps.new_matcher_io()), + "old_file shouldn't be newer than new_dir"); + assert!(matcher_for_old.matches(&new_file, &mut deps.new_matcher_io()), + "new_file should be newer than old_dir"); + assert!(!matcher_for_old.matches(&old_file, &mut deps.new_matcher_io()), + "old_file shouldn't be newer than itself"); + } + + #[test] + fn file_time_matcher() { + // this file should already exist + let file = get_dir_entry_for("test_data", "simple"); + + let files_mtime = file.metadata().unwrap().modified().unwrap(); + + let exactly_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, + ComparableValue::EqualTo(1)); + let more_than_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, + ComparableValue::MoreThan(1)); + let less_than_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, + ComparableValue::LessThan(1)); + let zero_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, + ComparableValue::EqualTo(0)); + + // set "now" to 2 days after the file was modified. + let mut deps = FakeDependencies::new(); + deps.set_time(files_mtime + Duration::new(2 * super::SECONDS_PER_DAY as u64, 0)); + assert!(!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "2 day old file shouldn't match exactly 1 day old"); + assert!(more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "2 day old file should match more than 1 day old"); + assert!(!less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "2 day old file shouldn't match less than 1 day old"); + assert!(!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "2 day old file shouldn't match exactly 0 days old"); + + // set "now" to 1 day after the file was modified. + deps.set_time(files_mtime + Duration::new((3 * super::SECONDS_PER_DAY / 2) as u64, 0)); + assert!(exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "1 day old file should match exactly 1 day old"); + assert!(!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "1 day old file shouldn't match more than 1 day old"); + assert!(!less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "1 day old file shouldn't match less than 1 day old"); + assert!(!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "1 day old file shouldn't match exactly 0 days old"); + + // set "now" to exactly the same time file was modified. + deps.set_time(files_mtime); + assert!(!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "0 day old file shouldn't match exactly 1 day old"); + assert!(!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "0 day old file shouldn't match more than 1 day old"); + assert!(less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "0 day old file should match less than 1 day old"); + assert!(zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "0 day old file should match exactly 0 days old"); + + + // set "now" to a second before the file was modified (e.g. the file was + // modified after find started running + deps.set_time(files_mtime - Duration::new(1 as u64, 0)); + assert!(!exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "future-modified file shouldn'1 match exactly 1 day old"); + assert!(!more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "future-modified file shouldn't match more than 1 day old"); + assert!(less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "future-modified file should match less than 1 day old"); + assert!(!zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), + "future-modified file shouldn't match exactly 0 days old"); + + } + + #[test] + fn file_time_matcher_modified_created_accessed() { + + let temp_dir = TempDir::new("file_time_matcher_modified_created_accessed").unwrap(); + + // No easy way to independently set file times. So create it - setting creation time + let foo_path = temp_dir.path().join("foo"); + { + File::create(&foo_path).expect("create temp file"); + } + + thread::sleep(Duration::from_secs(2)); + // read the file - potentially changing accessed time + let mut buffer = [0; 10]; + { + let mut f = File::open(&foo_path).expect("open temp file"); + let _ = f.read(&mut buffer); + } + + thread::sleep(Duration::from_secs(2)); + // write to the file - changing the modifiation and potentially the accessed time + let mut buffer = [0; 10]; + { + let mut f = + OpenOptions::new().read(true).write(true).open(&foo_path).expect("open temp file"); + let _ = f.write(&mut buffer); + } + + + thread::sleep(Duration::from_secs(2)); + // read the file agaion - potentially changing accessed time + { + let mut f = File::open(&foo_path).expect("open temp file"); + let _ = f.read(&mut buffer); + } + + // OK our modification time and creation time should definitely be different + // and depending on our platform and file system, our accessed time migh be + // different too. + + let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo"); + let metadata = file_info.metadata().unwrap(); + + // metadata can return errors like StringError("creation time is not available on this platform currently") + // so skip tests that won't pass due to shortcomings in std:;fs. + if let Ok(accessed_time) = metadata.accessed() { + test_matcher_for_file_time_type(&file_info, accessed_time, FileTimeType::Accessed); + } + + if let Ok(creation_time) = metadata.created() { + test_matcher_for_file_time_type(&file_info, creation_time, FileTimeType::Created); + } + + if let Ok(modified_time) = metadata.modified() { + test_matcher_for_file_time_type(&file_info, modified_time, FileTimeType::Modified); + } + } + + /// helper function for file_time_matcher_modified_created_accessed + fn test_matcher_for_file_time_type(file_info: &DirEntry, + file_time: SystemTime, + file_time_type: FileTimeType) { + { + let matcher = FileTimeMatcher::new(file_time_type, ComparableValue::EqualTo(0)); + + let mut deps = FakeDependencies::new(); + deps.set_time(file_time); + assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()), + "{:?} time matcher should match", + file_time_type); + + deps.set_time(file_time - Duration::from_secs(1)); + assert!(!matcher.matches(&file_info, &mut deps.new_matcher_io()), + "{:?} time matcher shouldn't match a second before", + file_time_type); + } + } +} diff --git a/src/find/matchers/type_matcher.rs b/src/find/matchers/type_matcher.rs new file mode 100644 index 0000000..9f4fd4e --- /dev/null +++ b/src/find/matchers/type_matcher.rs @@ -0,0 +1,81 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +use std::error::Error; +use std::fs::FileType; +use walkdir::DirEntry; + +use find::matchers::{Matcher, MatcherIO}; + +/// This matcher checks the type of the file. +pub struct TypeMatcher { + file_type_fn: fn(&FileType) -> bool, +} + +impl TypeMatcher { + pub fn new(type_string: &str) -> Result> { + let function = match type_string { + "f" => FileType::is_file, + "d" => FileType::is_dir, + "b" | "c" | "p" | "l" | "s" | "D" => { + return Err(From::from(format!("Type argument {} not supported yet", type_string))) + } + _ => return Err(From::from(format!("Unrecognised type argument {}", type_string))), + }; + Ok(TypeMatcher { file_type_fn: function }) + } + + pub fn new_box(type_string: &str) -> Result, Box> { + Ok(Box::new(TypeMatcher::new(type_string)?)) + } +} + +impl Matcher for TypeMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + (self.file_type_fn)(&file_info.file_type()) + } + + fn has_side_effects(&self) -> bool { + false + } +} +#[cfg(test)] + +mod tests { + use find::matchers::Matcher; + use find::matchers::tests::get_dir_entry_for; + use find::tests::FakeDependencies; + use super::*; + + #[test] + fn file_type_matcher() { + let file = get_dir_entry_for("test_data/simple", "abbbc"); + let dir = get_dir_entry_for("test_data", "simple"); + let deps = FakeDependencies::new(); + + let matcher = TypeMatcher::new(&"f".to_string()).unwrap(); + assert!(!matcher.matches(&dir, &mut deps.new_matcher_io())); + assert!(matcher.matches(&file, &mut deps.new_matcher_io())); + } + + #[test] + fn dir_type_matcher() { + let file = get_dir_entry_for("test_data/simple", "abbbc"); + let dir = get_dir_entry_for("test_data", "simple"); + let deps = FakeDependencies::new(); + + let matcher = TypeMatcher::new(&"d".to_string()).unwrap(); + assert!(matcher.matches(&dir, &mut deps.new_matcher_io())); + assert!(!matcher.matches(&file, &mut deps.new_matcher_io())); + } + + #[test] + fn cant_create_with_invalid_pattern() { + let result = TypeMatcher::new(&"xxx".to_string()); + assert!(result.is_err()); + } + +} diff --git a/src/find/mod.rs b/src/find/mod.rs new file mode 100644 index 0000000..337151a --- /dev/null +++ b/src/find/mod.rs @@ -0,0 +1,502 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +mod matchers; + +use std::cell::RefCell; +use std::error::Error; +use std::io::{Write, stderr, stdout}; +use std::rc::Rc; +use std::time::SystemTime; +use walkdir::WalkDir; +use walkdir::WalkDirIterator; + +pub struct Config { + depth_first: bool, + min_depth: usize, + max_depth: usize, + sorted_output: bool, +} + +impl Default for Config { + fn default() -> Config { + Config { + depth_first: false, + min_depth: 0, + max_depth: usize::max_value(), + sorted_output: false, + } + } +} + + +/// Trait that encapsulates various dependencies (output, clocks, etc.) that we +/// might want to fake out for unit tests. +pub trait Dependencies<'a> { + fn get_output(&'a self) -> &'a RefCell; + fn now(&'a self) -> SystemTime; +} + +/// Struct that holds the dependencies we use when run as the real executable. +pub struct StandardDependencies { + output: Rc>, + now: SystemTime, +} + +impl StandardDependencies { + pub fn new() -> StandardDependencies { + StandardDependencies { + output: Rc::new(RefCell::new(stdout())), + now: SystemTime::now(), + } + } +} + +impl<'a> Dependencies<'a> for StandardDependencies { + fn get_output(&'a self) -> &'a RefCell { + self.output.as_ref() + } + + fn now(&'a self) -> SystemTime { + self.now + } +} + +/// The result of parsing the command-line arguments into useful forms. +struct ParsedInfo { + matcher: Box, + paths: Vec, + config: Config, +} + +/// Function to generate a ParsedInfo from the strings supplied on the command-line. +fn parse_args(args: &[&str]) -> Result> { + let mut paths = vec![]; + let mut i = 0; + let mut config = Config::default(); + + while i < args.len() && !args[i].starts_with('-') && args[i] != "!" && args[i] != "(" { + paths.push(args[i].to_string()); + i += 1; + } + if i == 0 { + paths.push(".".to_string()); + } + let matcher = matchers::build_top_level_matcher(&args[i..], &mut config)?; + Ok(ParsedInfo { + matcher: matcher, + paths: paths, + config: config, + }) +} + +fn process_dir<'a>(dir: &str, + config: &Config, + deps: &'a Dependencies<'a>, + matcher: &Box) + -> Result> { + + let mut found_count: u64 = 0; + let mut walkdir = WalkDir::new(dir) + .contents_first(config.depth_first) + .max_depth(config.max_depth) + .min_depth(config.min_depth); + if config.sorted_output { + walkdir = walkdir.sort_by(|a, b| a.cmp(b)); + } + + // Slighly yucky loop handling here :-(. See docs for + // WalkDirIterator::skip_current_dir for explanation. + let mut it = walkdir.into_iter(); + loop { + match it.next() { + None => break, + Some(Err(err)) => { + writeln!(&mut stderr(), "Error: {}: {}", dir, err.description()).unwrap() + } + Some(Ok(entry)) => { + let mut matcher_io = matchers::MatcherIO::new(deps); + if matcher.matches(&entry, &mut matcher_io) { + found_count += 1; + } + if matcher_io.should_skip_current_dir() { + it.skip_current_dir(); + } + } + } + } + Ok(found_count) +} + + +fn do_find<'a>(args: &[&str], deps: &'a Dependencies<'a>) -> Result> { + let paths_and_matcher = parse_args(args)?; + let mut found_count: u64 = 0; + for path in paths_and_matcher.paths { + found_count += process_dir(&path, + &paths_and_matcher.config, + deps, + &paths_and_matcher.matcher)?; + } + Ok(found_count) +} + +fn print_help() { + println!("Usage: find [path...] [expression] + +If no path is supplied then the current working directory is used by default. + +Early alpha implementation. Currently the only expressions supported are + -print + -name case-sensitive_filename_pattern + -iname case-insensitive_filename_pattern + -type type_char + currently type_char can only be f (for file) or d (for directory) + -size [+-]N[bcwkMG] + -prune + -not + -a + -o[r] + , + () + -true + -false + -maxdepth N + -mindepth N + -d[epth] + -ctime [+-]N + -atime [+-]N + -mtime [+-]N + -newer path_to_file + -sorted + a non-standard extension that sorts directory contents by name before + processing them. Less efficient, but allows for deterministic output. +"); +} + +/// Does all the work for find. +/// +/// All main has to do is pass in the command-line args and exit the process +/// with the exit code. Note that the first string in args is expected to be +/// the name of the executable. +pub fn find_main<'a>(args: &[&str], deps: &'a Dependencies<'a>) -> i32 { + + for arg in args { + match arg.as_ref() { + "-help" | "--help" => { + print_help(); + return 0; + } + _ => (), + } + } + match do_find(&args[1..], deps) { + Ok(_) => 0, + Err(e) => { + writeln!(&mut stderr(), "Error: {}", e).unwrap(); + 1 + } + } +} + +#[cfg(test)] +mod tests { + + + use std::cell::RefCell; + use std::fs; + use std::io::{Cursor, Read, Write}; + use std::time::{Duration, SystemTime}; + use std::vec::Vec; + use tempdir::TempDir; + + use find::matchers::MatcherIO; + + use super::*; + + /// A struct that implements Dependencies, but uses faked implementations, + /// allowing us to check output, set the time returned by clocks etc. + pub struct FakeDependencies { + pub output: RefCell>>, + now: SystemTime, + } + + impl<'a> FakeDependencies { + pub fn new() -> FakeDependencies { + FakeDependencies { + output: RefCell::new(Cursor::new(Vec::::new())), + now: SystemTime::now(), + } + } + + pub fn set_time(&mut self, new_time: SystemTime) { + self.now = new_time; + } + + pub fn new_matcher_io(&'a self) -> MatcherIO<'a> { + MatcherIO::new(self) + } + + pub fn get_output_as_string(&self) -> String { + let mut cursor = self.output.borrow_mut(); + cursor.set_position(0); + let mut contents = String::new(); + cursor.read_to_string(&mut contents).unwrap(); + contents + } + } + + impl<'a> Dependencies<'a> for FakeDependencies { + fn get_output(&'a self) -> &'a RefCell { + &self.output + } + + fn now(&'a self) -> SystemTime { + self.now + } + } + + + #[test] + fn find_main_not_depth_first() { + let deps = FakeDependencies::new(); + + + let rc = find_main(&["find", "./test_data/simple", "-sorted"], &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/simple\n\ + ./test_data/simple/abbbc\n\ + ./test_data/simple/subdir\n\ + ./test_data/simple/subdir/ABBBC\n"); + } + + #[test] + fn find_main_depth_first() { + let deps = FakeDependencies::new(); + + + let rc = find_main(&["find", "./test_data/simple", "-depth"], &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/simple/subdir/ABBBC\n\ + ./test_data/simple/subdir\n\ + ./test_data/simple/abbbc\n\ + ./test_data/simple\n"); + } + + #[test] + fn find_maxdepth() { + let deps = FakeDependencies::new(); + + let rc = find_main(&["find", "./test_data/depth", "-sorted", "-maxdepth", "2"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/depth\n\ + ./test_data/depth/1\n\ + ./test_data/depth/1/2\n\ + ./test_data/depth/1/f1\n\ + ./test_data/depth/f0\n"); + } + + #[test] + fn find_maxdepth_depth_first() { + let deps = FakeDependencies::new(); + + let rc = find_main(&["find", "./test_data/depth", "-maxdepth", "2", "-depth"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/depth/1/2\n\ + ./test_data/depth/1/f1\n\ + ./test_data/depth/1\n\ + ./test_data/depth/f0\n\ + ./test_data/depth\n"); + } + + #[test] + fn find_prune() { + let deps = FakeDependencies::new(); + + let rc = find_main(&["find", + "./test_data/depth", + "-sorted", + "-print", + ",", + "-name", + "1", + "-prune"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/depth\n\ + ./test_data/depth/1\n\ + ./test_data/depth/f0\n"); + } + + #[test] + fn find_zero_maxdepth() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/depth", "-maxdepth", "0"], &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), "./test_data/depth\n"); + } + + #[test] + fn find_zero_maxdepth_depth_first() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/depth", "-maxdepth", "0", "-depth"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), "./test_data/depth\n"); + } + + #[test] + fn find_mindepth() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/depth", "-mindepth", "3"], &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/depth/1/2/3\n\ + ./test_data/depth/1/2/3/f3\n\ + ./test_data/depth/1/2/f2\n"); + } + + #[test] + fn find_mindepth_depth_first() { + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/depth", "-mindepth", "3", "-depth"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/depth/1/2/3/f3\n\ + ./test_data/depth/1/2/3\n\ + ./test_data/depth/1/2/f2\n"); + } + + #[test] + fn find_newer() { + // create a temp directory and file that are newer than the static + // files in the source tree. + let new_dir = TempDir::new("find_newer").unwrap(); + + let deps = FakeDependencies::new(); + + + let rc = find_main(&["find", + &new_dir.path().to_string_lossy(), + "-newer", + "./test_data/simple/abbbc"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + (&new_dir).path().to_string_lossy().to_string() + "\n"); + + // now do it the other way around, and nothing should be output + let deps = FakeDependencies::new(); + let rc = find_main(&["find", + "./test_data/simple/abbbc", + "-newer", + &new_dir.path().to_string_lossy()], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), ""); + } + + #[test] + fn find_mtime() { + let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); + + // metadata can return errors like StringError("creation time is not available on this platform currently") + // so skip tests that won't pass due to shortcomings in std:;fs. + if let Ok(file_time) = meta.modified() { + file_time_helper(file_time, "-mtime"); + } + } + + #[test] + fn find_ctime() { + let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); + + // metadata can return errors like StringError("creation time is not available on this platform currently") + // so skip tests that won't pass due to shortcomings in std:;fs. + if let Ok(file_time) = meta.created() { + file_time_helper(file_time, "-ctime"); + } + } + + #[test] + fn find_atime() { + let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); + + // metadata can return errors like StringError("creation time is not available on this platform currently") + // so skip tests that won't pass due to shortcomings in std:;fs. + if let Ok(file_time) = meta.accessed() { + file_time_helper(file_time, "-atime"); + } + } + + /// Helper function for the find_ctime/find_atime/find_mtime tests. + fn file_time_helper(file_time: SystemTime, arg: &str) { + // check file time matches a file that's old enough + { + let mut deps = FakeDependencies::new(); + deps.set_time(file_time); + + let rc = find_main(&["find", "./test_data/simple/subdir", "-type", "f", arg, "0"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), + "./test_data/simple/subdir/ABBBC\n"); + } + + // now Check file time doesn't match a file that's too new + { + let mut deps = FakeDependencies::new(); + deps.set_time(file_time - Duration::from_secs(1)); + + let rc = find_main(&["find", "./test_data/simple/subdir", "-type", "f", arg, "0"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), ""); + } + } + + #[test] + fn find_size() { + let deps = FakeDependencies::new(); + // only look at files because the "size" of a directory is a system (and filesystem) + // dependent thing and we want these tests to be universal. + let rc = find_main(&["find", "./test_data/size", "-type", "f", "-size", "1b"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), "./test_data/size/512bytes\n"); + + let deps = FakeDependencies::new(); + let rc = find_main(&["find", "./test_data/size", "-type", "f", "-size", "+1b"], + &deps); + + assert_eq!(rc, 0); + assert_eq!(deps.get_output_as_string(), ""); + + } + +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cf42ad4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2017 Google Inc. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +extern crate glob; +extern crate regex; +extern crate walkdir; + +#[cfg(test)] +extern crate tempdir; + +pub mod find; diff --git a/test_data/depth/1/2/3/f3 b/test_data/depth/1/2/3/f3 new file mode 100644 index 0000000..e69de29 diff --git a/test_data/depth/1/2/f2 b/test_data/depth/1/2/f2 new file mode 100644 index 0000000..e69de29 diff --git a/test_data/depth/1/f1 b/test_data/depth/1/f1 new file mode 100644 index 0000000..e69de29 diff --git a/test_data/depth/f0 b/test_data/depth/f0 new file mode 100644 index 0000000..e69de29 diff --git a/test_data/simple/abbbc b/test_data/simple/abbbc new file mode 100644 index 0000000..e69de29 diff --git a/test_data/simple/subdir/ABBBC b/test_data/simple/subdir/ABBBC new file mode 100644 index 0000000..e69de29 diff --git a/test_data/size/512bytes b/test_data/size/512bytes new file mode 100644 index 0000000000000000000000000000000000000000..a64a5a93fb4aef4d5f63d79cb2582731b9ac5063 GIT binary patch literal 512 McmZP=1tU5H00IC20RR91 literal 0 HcmV?d00001