├── .gitignore ├── Cargo.toml ├── README.md ├── examples └── dflemstr.rs ├── image └── realize-screenshot.png └── src ├── error.rs ├── fs.rs ├── lib.rs ├── meta.rs ├── resource.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["David Flemström "] 3 | categories = [ 4 | "config", 5 | "command-line-utilities", 6 | ] 7 | description = "A blazingly fast configuration management library" 8 | documentation = "https://docs.rs/realize" 9 | homepage = "https://github.com/dflemstr/realize" 10 | keywords = [ 11 | "configuration", 12 | "administration", 13 | ] 14 | license = "Apache-2.0" 15 | name = "realize" 16 | readme = "README.md" 17 | repository = "https://github.com/dflemstr/realize" 18 | version = "0.1.0" 19 | 20 | [dependencies] 21 | linked-hash-map = "0.5.1" 22 | sha1 = "0.6.0" 23 | slog-async = "2.3.0" 24 | slog-journald = "2.0.0" 25 | slog-term = "2.4.0" 26 | failure = "0.1.2" 27 | 28 | [dependencies.slog] 29 | features = [ 30 | "release_max_level_trace", 31 | "max_level_trace", 32 | ] 33 | version = "2.3.2" 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `realize` [![crates.io badge](https://img.shields.io/crates/v/realize.svg)](https://crates.io/crates/realize) [![docs.rs badge](https://docs.rs/realize/badge.svg)](https://docs.rs/realize) 2 | 3 | `realize` is a blazingly fast configuration management library written in Rust. 4 | It exposes a type-safe eDSL for writing system configuration programs. 5 | 6 | # Features 7 | 8 | The goal of `realize` is to allow you to write flexible system configurations 9 | using the full power of Rust. You can then deploy this configuration to any 10 | other machine as a statically linked executable and run it to apply needed 11 | changes without having to install any other dependencies. 12 | 13 | This is different from other similar configuration management tools such as 14 | Ansible, Puppet or Chef, which have significant system dependencies, and 15 | interpret the configuration on the target machine in a dynamic way, which can 16 | lead to errors that a type system can alleviate. 17 | 18 | # Example 19 | 20 | Here is an example `realize` configuration (see the `examples` directory for 21 | more): 22 | 23 | ```rust 24 | extern crate realize; 25 | 26 | use realize::fs; 27 | 28 | fn main() { 29 | realize::apply(configuration) 30 | } 31 | 32 | fn configuration(reality: &mut realize::Reality) { 33 | reality.ensure(fs::File::at("/etc/hostname").contains_str("dflemstr-desktop")); 34 | // Include the ’files/etc/passwd’ file in the static binary so that the 35 | // configuration is truly dependency-free 36 | reality.ensure(fs::File::at("/etc/passwd").contains_str(include_str!("files/etc/passwd"))); 37 | } 38 | ``` 39 | 40 | Example output (not from the above example, but another one): 41 | 42 | ![example output](./image/realize-screenshot.png) 43 | 44 | # Getting started 45 | 46 | [Install Rust](https://www.rust-lang.org/install.html) (only needed on your 47 | development machine) and then create a new crate for your configuration: 48 | 49 | $ cargo new --bin myconfig 50 | $ cd myconfig 51 | 52 | Now, declare a dependency on `realize` in your `Cargo.toml` file: 53 | 54 | [dependencies] 55 | realize = "*" 56 | 57 | You’re now ready to put your configuration in `src/main.rs`. To apply the configuration locally, just run: 58 | 59 | $ cargo run 60 | 61 | To get a binary that can be deployed elsewhere, build a release binary: 62 | 63 | $ cargo build --release 64 | 65 | The resulting binary is in `target/release/myconfig`. 66 | 67 | This binary still depends on your operating system’s LIBC version. To get a 68 | truly dependency free binary, first install [musl](https://www.musl-libc.org/) 69 | using your operating system’s package manager, then cross compile the binary to 70 | musl: 71 | 72 | $ rustup target add x86_64-unknown-linux-musl 73 | $ cargo build --release --target x86_64-unknown-linux-musl 74 | -------------------------------------------------------------------------------- /examples/dflemstr.rs: -------------------------------------------------------------------------------- 1 | extern crate realize; 2 | 3 | use realize::fs; 4 | 5 | fn main() { 6 | realize::apply(configuration) 7 | } 8 | 9 | fn configuration(reality: &mut realize::Reality) { 10 | reality.ensure(fs::File::at("/tmp/test").contains_str("hello")); 11 | } 12 | -------------------------------------------------------------------------------- /image/realize-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflemstr/realize/dd7b0628b6c8e8c9934a1ce4ed1583cf128bda16/image/realize-screenshot.png -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Consistent definitions for realize error types. 2 | use failure; 3 | use std::result; 4 | 5 | /// A result whose error is `Error`. 6 | pub type Result = result::Result; 7 | 8 | /// An error originating from a realize operation. 9 | pub type Error = failure::Error; 10 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | //! File-system related resource types. 2 | use std::any; 3 | use std::fmt; 4 | use std::fs; 5 | use std::io; 6 | use std::os; 7 | use std::path; 8 | 9 | use error; 10 | use resource; 11 | use util; 12 | 13 | /// A file, which can be absent, a regular file, a directory, or a symlink. 14 | #[derive(Debug, Eq, PartialEq)] 15 | pub struct File { 16 | path: path::PathBuf, 17 | file_type: FileType, 18 | } 19 | 20 | #[derive(Debug, Eq, PartialEq)] 21 | enum FileType { 22 | Absent, 23 | File { contents: Option> }, 24 | Dir, 25 | Symlink { target: path::PathBuf }, 26 | } 27 | 28 | impl File { 29 | /// Starts reasoning about a file at a certain path. The resulting resource 30 | /// will only ensure that the file exists and is a regular file. 31 | pub fn at

(path: P) -> File 32 | where 33 | P: Into, 34 | { 35 | File { 36 | path: path.into(), 37 | file_type: FileType::File { contents: None }, 38 | } 39 | } 40 | 41 | /// The file should contain the specified byte contents. It is recommended 42 | /// to call this method with either a binary literal (`contains(b"abc")`) or 43 | /// with an embedded external file (`contains(include_bytes!("my/file"))`). 44 | pub fn contains(mut self, contents: B) -> File 45 | where 46 | B: Into>, 47 | { 48 | self.file_type = FileType::File { 49 | contents: Some(contents.into()), 50 | }; 51 | self 52 | } 53 | 54 | /// The file should contain the specified string contents, encoded as UTF-8. 55 | /// It is recommended to call this method with either a string literal 56 | /// (`contains("abc")`) or with an embedded external file 57 | /// (`contains(include_str!("my/file"))`). 58 | pub fn contains_str(self, contents: S) -> File 59 | where 60 | S: Into, 61 | { 62 | self.contains(contents.into().into_bytes()) 63 | } 64 | 65 | /// The file is a regular file, with any contents. 66 | pub fn is_file(mut self) -> File { 67 | self.file_type = FileType::File { contents: None }; 68 | self 69 | } 70 | 71 | /// The file is a directory. 72 | pub fn is_dir(mut self) -> File { 73 | self.file_type = FileType::Dir; 74 | self 75 | } 76 | 77 | /// The file is a symlink that points to the supplied path. 78 | pub fn points_to

(mut self, path: P) -> File 79 | where 80 | P: Into, 81 | { 82 | self.file_type = FileType::Symlink { 83 | target: path.into(), 84 | }; 85 | self 86 | } 87 | 88 | /// The file should be absent, and will be deleted if it exists. 89 | pub fn is_absent(mut self) -> File { 90 | self.file_type = FileType::Absent; 91 | self 92 | } 93 | } 94 | 95 | impl resource::Resource for File { 96 | fn key(&self) -> resource::Key { 97 | resource::Key::Path(self.path.clone()) 98 | } 99 | 100 | fn realize(&self, &resource::Context { log, .. }: &resource::Context) -> error::Result<()> { 101 | use failure::ResultExt; 102 | 103 | let path = self.path.to_string_lossy().into_owned(); 104 | let log = log.new(o!("path" => path)); 105 | 106 | match self.file_type { 107 | FileType::Absent => { 108 | if self.path.is_dir() { 109 | trace!(log, "Deleting directory"); 110 | fs::remove_dir(&self.path) 111 | .with_context(|_| format!("Failed to delete directory {:?}", self.path))?; 112 | } 113 | if self.path.is_file() { 114 | trace!(log, "Deleting file"); 115 | fs::remove_file(&self.path) 116 | .with_context(|_| format!("Failed to delete file {:?}", self.path))?; 117 | } 118 | } 119 | FileType::File { ref contents } => { 120 | if let Some(ref contents) = *contents { 121 | use std::io::Write; 122 | 123 | trace!(log, "Updating file contents"); 124 | let mut f = fs::File::create(&self.path) 125 | .with_context(|_| format!("Failed to create file {:?}", self.path))?; 126 | f.write_all(contents) 127 | .with_context(|_| format!("Failed to write to file {:?}", self.path))?; 128 | } 129 | } 130 | FileType::Dir => { 131 | fs::create_dir_all(&self.path) 132 | .with_context(|_| format!("Failed to create directory {:?}", self.path))?; 133 | } 134 | FileType::Symlink { ref target } => { 135 | // TODO: add support for other OSes 136 | os::unix::fs::symlink(target, &self.path) 137 | .with_context(|_| format!("Failed to create symlink {:?}", self.path))?; 138 | } 139 | } 140 | Ok(()) 141 | } 142 | 143 | fn verify(&self, &resource::Context { log, .. }: &resource::Context) -> error::Result { 144 | use failure::ResultExt; 145 | 146 | let log = log.new(o!("path" => self.path.to_string_lossy().into_owned())); 147 | 148 | if !self.path.exists() { 149 | debug!(log, "Path does not exist"); 150 | return Ok(false); 151 | } 152 | 153 | let metadata = fs::metadata(&self.path) 154 | .with_context(|_| format!("Failed to gather metadata about path {:?}", self.path))?; 155 | match self.file_type { 156 | FileType::File { ref contents } => { 157 | if !metadata.file_type().is_file() { 158 | debug!(log, "Path doesn't point to a regular file"); 159 | return Ok(false); 160 | } 161 | 162 | if let Some(ref contents) = *contents { 163 | let file = fs::File::open(&self.path).with_context(|_| { 164 | format!("Failed to open file {:?} for hashing", self.path) 165 | })?; 166 | let old_sha1 = util::sha1(file) 167 | .with_context(|_| { 168 | format!("Failed to compute SHA-1 digest of file {:?}", self.path) 169 | })? 170 | .to_string(); 171 | let new_sha1 = util::sha1(io::Cursor::new(contents)) 172 | .with_context(|_| "Failed to compute SHA-1 digest")? 173 | .to_string(); 174 | if old_sha1 != new_sha1 { 175 | debug!(log, "File has wrong contents"; 176 | "old_sha1" => old_sha1, "new_sha1" => new_sha1); 177 | return Ok(false); 178 | } 179 | } 180 | } 181 | FileType::Dir => { 182 | if !metadata.file_type().is_dir() { 183 | debug!(log, "Path doesn't point to a directory"); 184 | return Ok(false); 185 | } 186 | } 187 | FileType::Symlink { 188 | target: ref new_target, 189 | } => { 190 | if !metadata.file_type().is_symlink() { 191 | debug!(log, "Path doesn't point to a symlink"); 192 | return Ok(false); 193 | } 194 | 195 | let old_target = fs::read_link(&self.path) 196 | .with_context(|_| format!("Failed to read link target of {:?}", self.path))?; 197 | if old_target != *new_target { 198 | let old_target = old_target.to_string_lossy().into_owned(); 199 | let new_target = new_target.to_string_lossy().into_owned(); 200 | debug!(log, "Symlink target is wrong"; 201 | "old_target" => old_target, "new_target" => new_target); 202 | return Ok(false); 203 | } 204 | } 205 | FileType::Absent => {} 206 | } 207 | 208 | trace!(log, "File is up to date"); 209 | Ok(true) 210 | } 211 | 212 | fn as_any(&self) -> &any::Any { 213 | self 214 | } 215 | } 216 | 217 | impl resource::UnresolvedResource for File { 218 | fn implicit_ensure(&self, ensurer: &mut E) 219 | where 220 | E: resource::Ensurer, 221 | { 222 | if let Some(parent) = self.path.parent() { 223 | ensurer.ensure(File::at(parent).is_dir()); 224 | } 225 | } 226 | } 227 | 228 | impl fmt::Display for File { 229 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 230 | match self.file_type { 231 | FileType::Absent { .. } => write!(f, "absent")?, 232 | FileType::File { .. } => write!(f, "file")?, 233 | FileType::Dir { .. } => write!(f, "directory")?, 234 | FileType::Symlink { .. } => write!(f, "symlink")?, 235 | } 236 | 237 | write!(f, " {:?}", self.path)?; 238 | 239 | match self.file_type { 240 | FileType::File { 241 | contents: Some(ref contents), 242 | } => { 243 | let sha1 = util::sha1(io::Cursor::new(contents)).unwrap(); 244 | write!(f, " with sha1 {}", &format!("{}", sha1)[..8])? 245 | } 246 | FileType::Symlink { ref target } => write!(f, " with target {:?}", target)?, 247 | _ => (), 248 | } 249 | Ok(()) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A blazingly fast configuration management library. Exposes a type-safe eDSL 2 | //! for writing system configuration programs. 3 | //! 4 | //! # Terminology 5 | //! 6 | //! This library realizes configurations. A configuration is a collection of 7 | //! `Resource`s, that are declared using an eDSL. When all resources have been 8 | //! realized, the configuration is considered applied. 9 | //! 10 | //! To aid with dependency management, there are meta-resources that can manage 11 | //! several resources and the dependencies between them. One such resource is 12 | //! the `Reality` resource, that can `ensure` that other resources are realized. 13 | 14 | #![deny( 15 | missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, 16 | trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, 17 | unused_qualifications 18 | )] 19 | 20 | extern crate failure; 21 | extern crate linked_hash_map; 22 | extern crate sha1; 23 | #[macro_use] 24 | extern crate slog; 25 | extern crate slog_async; 26 | extern crate slog_journald; 27 | extern crate slog_term; 28 | 29 | pub mod error; 30 | pub mod resource; 31 | mod util; 32 | 33 | pub mod fs; 34 | pub mod meta; 35 | 36 | pub use meta::Reality; 37 | 38 | use std::process; 39 | 40 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 41 | 42 | /// Runs the supplied configuration function and applies the resulting reality. 43 | /// This should be called from your `main` method, it assumes that it is safe to 44 | /// call `std::process::exit` for example. 45 | pub fn apply(config: F) 46 | where 47 | F: FnOnce(&mut Reality), 48 | { 49 | use slog::Drain; 50 | let decorator = slog_term::TermDecorator::new().stderr().build(); 51 | let term_drain = slog_term::CompactFormat::new(decorator) 52 | .use_utc_timestamp() 53 | .build() 54 | .fuse(); 55 | let term_drain = slog_async::Async::new(term_drain).build().fuse(); 56 | let journald_drain = slog_journald::JournaldDrain.fuse(); 57 | 58 | let drain = slog::Duplicate::new(term_drain, journald_drain).fuse(); 59 | let log = slog::Logger::root(drain.fuse(), o!("realize-version" => VERSION)); 60 | 61 | match apply_inner(&log, config) { 62 | Ok(()) => (), 63 | Err(e) => { 64 | error!(log, "{}", e); 65 | for error in e.iter_causes() { 66 | error!(log, " → {}", error); 67 | } 68 | // Ensure we log everything 69 | drop(log); 70 | process::exit(1); 71 | } 72 | } 73 | } 74 | 75 | fn apply_inner(log: &slog::Logger, config: F) -> error::Result<()> 76 | where 77 | F: FnOnce(&mut Reality), 78 | { 79 | use resource::Resource; 80 | 81 | let context = resource::Context { log: log }; 82 | 83 | let mut resource = Reality::new(log.clone()); 84 | 85 | config(&mut resource); 86 | 87 | info!(log, "Applying configuration"); 88 | if resource.verify(&context)? { 89 | info!(log, "Everything up to date, nothing to do"); 90 | } else { 91 | resource.realize(&context)?; 92 | info!(log, "Configuration applied"); 93 | } 94 | Ok(()) 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | #[test] 100 | fn it_works() {} 101 | } 102 | -------------------------------------------------------------------------------- /src/meta.rs: -------------------------------------------------------------------------------- 1 | //! Meta-resources, that manage other resources. 2 | use std::any; 3 | use std::fmt; 4 | 5 | use linked_hash_map; 6 | use slog; 7 | 8 | use error; 9 | use resource; 10 | 11 | /// A meta-resource that ensures that other resources are realized, in the 12 | /// correct dependency order. 13 | #[derive(Debug)] 14 | pub struct Reality { 15 | log: slog::Logger, 16 | resources: 17 | linked_hash_map::LinkedHashMap<(any::TypeId, resource::Key), Box>, 18 | } 19 | 20 | impl Reality { 21 | /// Constructs a new reality that logs to the specified logger. 22 | pub fn new(log: slog::Logger) -> Reality { 23 | Reality { 24 | log: log, 25 | resources: linked_hash_map::LinkedHashMap::new(), 26 | } 27 | } 28 | 29 | /// Adds a resource to be realized (including its dependencies) when this 30 | /// reality is realized. 31 | pub fn ensure(&mut self, resource: R) 32 | where 33 | R: resource::UnresolvedResource + resource::Resource + 'static, 34 | { 35 | resource.implicit_ensure(self); 36 | let key = (any::TypeId::of::(), resource.key()); 37 | 38 | match self.resources.entry(key) { 39 | linked_hash_map::Entry::Occupied(entry) => { 40 | // This unwrap() should never panic since we use TypeId::of::() as part of the 41 | // key 42 | let existing = entry.get().as_any().downcast_ref::().unwrap(); 43 | if *existing != resource { 44 | let key = format!("{}", entry.key().1); 45 | let old = format!("{}", existing); 46 | let new = format!("{}", resource); 47 | warn!(self.log, "Duplicate resource definitions; will use the older one"; 48 | "key" => key, "old" => old, "new" => new); 49 | } 50 | } 51 | linked_hash_map::Entry::Vacant(entry) => { 52 | entry.insert(Box::new(resource)); 53 | } 54 | } 55 | } 56 | } 57 | 58 | impl resource::Resource for Reality { 59 | fn key(&self) -> resource::Key { 60 | resource::Key::Seq(self.resources.values().map(|r| r.key()).collect()) 61 | } 62 | 63 | fn realize(&self, ctx: &resource::Context) -> error::Result<()> { 64 | use failure::ResultExt; 65 | 66 | for (_, resource) in &self.resources { 67 | resource 68 | .realize(ctx) 69 | .with_context(|_| format!("Could not realize {}", resource))?; 70 | } 71 | 72 | Ok(()) 73 | } 74 | 75 | fn verify(&self, ctx: &resource::Context) -> error::Result { 76 | use failure::ResultExt; 77 | 78 | for (_, resource) in &self.resources { 79 | if !resource 80 | .verify(ctx) 81 | .with_context(|_| format!("Could not verify {}", resource))? 82 | { 83 | return Ok(false); 84 | } 85 | } 86 | 87 | Ok(true) 88 | } 89 | 90 | fn as_any(&self) -> &any::Any { 91 | self 92 | } 93 | } 94 | 95 | impl resource::Ensurer for Reality { 96 | fn ensure(&mut self, resource: R) 97 | where 98 | R: resource::UnresolvedResource + 'static, 99 | { 100 | Reality::ensure(self, resource) 101 | } 102 | } 103 | 104 | impl fmt::Display for Reality { 105 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 106 | write!(f, "reality") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/resource.rs: -------------------------------------------------------------------------------- 1 | //! Definitions for resources and related types. 2 | use std::any; 3 | use std::collections; 4 | use std::fmt; 5 | use std::path; 6 | 7 | use slog; 8 | 9 | use error; 10 | 11 | /// Something that is managed by `realize` and that can be realized on the 12 | /// target system. 13 | pub trait Resource: fmt::Debug + fmt::Display { 14 | /// The key of this resource, that distinguishes it from other resources of 15 | /// the same type. For files, this might be the file path, for example. 16 | fn key(&self) -> Key; 17 | 18 | /// Realizes this resource, performing any needed operations on the target 19 | /// system. 20 | fn realize(&self, ctx: &Context) -> error::Result<()>; 21 | 22 | /// Verifies whether this resource is already realized on the target system. 23 | /// This will potentially perform a lot of IO operations but should not make 24 | /// any changes. 25 | fn verify(&self, ctx: &Context) -> error::Result; 26 | 27 | /// Converts this resource into an opaque `Any` reference. 28 | fn as_any(&self) -> &any::Any; 29 | } 30 | 31 | /// A resource key, used to uniquely disambiguate resources of the same type. 32 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 33 | pub enum Key { 34 | /// A composite key consisting of several fields. 35 | Map(collections::BTreeMap), 36 | /// A composite key consisting of several ordered keys. 37 | Seq(Vec), 38 | /// A key that is based on a string value. 39 | String(String), 40 | /// A key that is based on a file system path. 41 | Path(path::PathBuf), 42 | // Add more stuff as needed 43 | } 44 | 45 | /// A `Resource` that is not yet "resolved," meaning that it might have 46 | /// dependency resources that should be implicitly realized as well. 47 | pub trait UnresolvedResource: Resource + Eq { 48 | /// Ensures that any implicit resources that this resource depends on are 49 | /// realized. 50 | #[allow(unused_variables)] 51 | fn implicit_ensure(&self, ensurer: &mut E) 52 | where 53 | E: Ensurer, 54 | { 55 | } 56 | } 57 | 58 | /// Something that can ensure that resources are realized. 59 | pub trait Ensurer { 60 | /// Ensures that the specified resource is realized, including its 61 | /// unresolved dependencies. 62 | fn ensure(&mut self, resource: R) 63 | where 64 | R: UnresolvedResource + 'static; 65 | } 66 | 67 | /// A context that is passed around various parts of the library, containing 68 | /// common functionality. 69 | #[derive(Debug)] 70 | pub struct Context<'a> { 71 | /// A structured logger to use when logging contextual information. 72 | pub log: &'a slog::Logger, 73 | } 74 | 75 | impl fmt::Display for Key { 76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 | match *self { 78 | Key::Map(ref value) => { 79 | write!(f, "{{")?; 80 | let mut needs_sep = false; 81 | for (key, value) in value { 82 | if needs_sep { 83 | write!(f, ", ")?; 84 | } 85 | write!(f, "{}: {}", key, value)?; 86 | needs_sep = true; 87 | } 88 | write!(f, "}}")?; 89 | } 90 | Key::Seq(ref value) => { 91 | write!(f, "[")?; 92 | let mut needs_sep = false; 93 | for value in value { 94 | if needs_sep { 95 | write!(f, ", ")?; 96 | } 97 | write!(f, "{}", value)?; 98 | needs_sep = true; 99 | } 100 | write!(f, "]")?; 101 | } 102 | Key::String(ref value) => write!(f, "{:?}", value)?, 103 | Key::Path(ref path) => write!(f, "{:?}", path)?, 104 | } 105 | Ok(()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use sha1; 4 | 5 | use error; 6 | 7 | pub fn sha1(mut read: R) -> error::Result 8 | where 9 | R: io::Read, 10 | { 11 | let mut sha1 = sha1::Sha1::new(); 12 | 13 | // SHA1 acts on 64-byte blocks, so this makes sense as a buffer size 14 | let mut buf = [0; 64]; 15 | 16 | loop { 17 | let n = read.read(&mut buf)?; 18 | 19 | if n == 0 { 20 | break; 21 | } 22 | 23 | sha1.update(&buf[..n]); 24 | } 25 | 26 | Ok(sha1.digest()) 27 | } 28 | --------------------------------------------------------------------------------