diff options
author | Tomasz Kramkowski <tomasz@kramkow.ski> | 2023-01-27 13:58:10 +0000 |
---|---|---|
committer | Tomasz Kramkowski <tomasz@kramkow.ski> | 2023-01-27 13:58:10 +0000 |
commit | 6cef9f0fc159de4c9fd708050ec76adb4e74d390 (patch) | |
tree | 81856b3b4da6d18e10516b0be737a157e13f129b | |
parent | 9e8dd00da25273fba9f0cafccbde2236e04fb24e (diff) | |
download | pam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.tar.gz pam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.tar.xz pam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.zip |
openat variant
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 14 | ||||
-rwxr-xr-x | install | 2 | ||||
-rw-r--r-- | openat/.gitignore | 4 | ||||
-rw-r--r-- | openat/.travis.yml | 35 | ||||
-rw-r--r-- | openat/Cargo.toml | 22 | ||||
-rw-r--r-- | openat/LICENSE-APACHE | 202 | ||||
-rw-r--r-- | openat/LICENSE-MIT | 19 | ||||
-rw-r--r-- | openat/README.md | 36 | ||||
-rw-r--r-- | openat/benches/count_processes.rs | 39 | ||||
-rw-r--r-- | openat/bulk.yaml | 8 | ||||
-rw-r--r-- | openat/examples/exchange.rs | 39 | ||||
-rw-r--r-- | openat/src/dir.rs | 696 | ||||
-rw-r--r-- | openat/src/filetype.rs | 33 | ||||
-rw-r--r-- | openat/src/lib.rs | 95 | ||||
-rw-r--r-- | openat/src/list.rs | 153 | ||||
-rw-r--r-- | openat/src/metadata.rs | 75 | ||||
-rw-r--r-- | openat/src/name.rs | 76 | ||||
-rw-r--r-- | openat/tests/tmpfile.rs | 24 | ||||
-rw-r--r-- | openat/vagga.yaml | 91 | ||||
-rw-r--r-- | src/lib.rs | 56 |
21 files changed, 1721 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48617e0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pam_usercg" +version = "0.1.0" +edition = "2021" + +[lib] +name = "pam_usercg" +crate-type = ["cdylib"] + +[dependencies] +libc = "0.2.139" +openat = { path = "openat" } +pam-bindings = "0.1.1" +users = "0.11.0" @@ -0,0 +1,2 @@ +#!/bin/sh +install -Dm755 target/debug/libpam_usercg.so "${DESTDIR}${PREFIX:-/usr/local}/lib/security/pam_usercg.so" diff --git a/openat/.gitignore b/openat/.gitignore new file mode 100644 index 0000000..afef0a7 --- /dev/null +++ b/openat/.gitignore @@ -0,0 +1,4 @@ +/.vagga +Cargo.lock +/target +/tmp diff --git a/openat/.travis.yml b/openat/.travis.yml new file mode 100644 index 0000000..503b688 --- /dev/null +++ b/openat/.travis.yml @@ -0,0 +1,35 @@ +sudo: false +dist: trusty +language: rust + +cache: +- cargo + +before_cache: +- rm -r $TRAVIS_BUILD_DIR/target/debug + +jobs: + include: + - os: linux + rust: stable + - os: linux + rust: beta + - os: linux + rust: nightly + - os: macos + osx_image: xcode9.3 + rust: stable + + # deploy + - stage: publish + os: linux + rust: stable + env: + install: true + script: true + + deploy: + - provider: script + script: 'cargo publish --verbose --token=$CARGO_TOKEN' + on: + tags: true diff --git a/openat/Cargo.toml b/openat/Cargo.toml new file mode 100644 index 0000000..a5b5b3f --- /dev/null +++ b/openat/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "openat" +description = """ + A wrapper around openat, symlinkat, and similar system calls +""" +license = "MIT/Apache-2.0" +readme = "README.md" +keywords = ["open", "openat", "filesystem", "fs"] +categories = ["filesystem", "api-bindings"] +repository = "https://github.com/tailhook/openat" +homepage = "https://github.com/tailhook/openat" +documentation = "http://docs.rs/openat" +version = "0.1.21" +authors = ["paul@colomiets.name"] +edition = "2018" + +[dependencies] +libc = "0.2.34" + +[dev-dependencies] +argparse = "0.2.1" +tempfile = "3.0.3" diff --git a/openat/LICENSE-APACHE b/openat/LICENSE-APACHE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/openat/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/openat/LICENSE-MIT b/openat/LICENSE-MIT new file mode 100644 index 0000000..dbd7f65 --- /dev/null +++ b/openat/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2016 The openat Developers + +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 the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/openat/README.md b/openat/README.md new file mode 100644 index 0000000..bf82045 --- /dev/null +++ b/openat/README.md @@ -0,0 +1,36 @@ +Openat Crate +============ + +**Status: Beta** + +[Documentation](https://docs.rs/openat) | +[Github](https://github.com/tailhook/openat) | +[Crate](https://crates.io/crates/openat) + + +The interface to ``openat``, ``symlinkat``, and other functions in ``*at`` +family. + +Dependent crates +================ + +This crate is a thin wrapper for the underlying system calls. +You may find the extension methods in [openat-ext](https://crates.io/crates/openat-ext) useful. + +License +======= + +Licensed under either of + +* Apache License, Version 2.0, + (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license (./LICENSE-MIT or http://opensource.org/licenses/MIT) + at your option. + +Contribution +------------ + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/openat/benches/count_processes.rs b/openat/benches/count_processes.rs new file mode 100644 index 0000000..aa9f856 --- /dev/null +++ b/openat/benches/count_processes.rs @@ -0,0 +1,39 @@ +#![feature(test)] + +extern crate openat; +extern crate test; + + +use std::fs::read_dir; +use std::str::from_utf8; +use std::os::unix::ffi::OsStrExt; +use test::Bencher; + +use openat::Dir; + + +#[bench] +fn procs_stdlib(b: &mut Bencher) { + b.iter(|| { + read_dir("/proc").unwrap().filter(|r| { + r.as_ref().ok() + .and_then(|e| from_utf8(e.file_name().as_bytes()).ok() + // pid is everything that can be parsed as a number + .and_then(|s| s.parse::<u32>().ok())) + .is_some() + }).count() + }); +} + +#[bench] +fn procs_openat(b: &mut Bencher) { + b.iter(|| { + Dir::open("/proc").unwrap().list_dir(".").unwrap().filter(|r| { + r.as_ref().ok() + .and_then(|e| from_utf8(e.file_name().as_bytes()).ok() + // pid is everything that can be parsed as a number + .and_then(|s| s.parse::<u32>().ok())) + .is_some() + }).count() + }); +} diff --git a/openat/bulk.yaml b/openat/bulk.yaml new file mode 100644 index 0000000..cdb9763 --- /dev/null +++ b/openat/bulk.yaml @@ -0,0 +1,8 @@ +minimum-bulk: v0.4.5 + +versions: + +- file: Cargo.toml + block-start: ^\[package\] + block-end: ^\[.*\] + regex: ^version\s*=\s*"(\S+)" diff --git a/openat/examples/exchange.rs b/openat/examples/exchange.rs new file mode 100644 index 0000000..21fdf0a --- /dev/null +++ b/openat/examples/exchange.rs @@ -0,0 +1,39 @@ +extern crate argparse; +extern crate openat; + +use std::process::exit; +use std::path::PathBuf; + +use argparse::{ArgumentParser, Parse}; +use openat::Dir; + +#[cfg(not(target_os="linux"))] +fn main() { + println!("Atomic exchange is not supported on this platform") +} + +#[cfg(target_os="linux")] +fn main() { + let mut path1 = PathBuf::new(); + let mut path2 = PathBuf::new(); + { + let mut ap = ArgumentParser::new(); + ap.refer(&mut path1) + .add_argument("path1", Parse, "First path of exchange operation") + .required(); + ap.refer(&mut path2) + .add_argument("path2", Parse, "Second path of exchange operation") + .required(); + ap.parse_args_or_exit(); + } + if path1.parent() != path2.parent() { + println!("Paths must be in the same directory"); + exit(1); + } + let parent = path1.parent().expect("path must have parent directory"); + let dir = Dir::open(parent).expect("can open directory"); + dir.local_exchange( + path1.file_name().expect("path1 must have filename"), + path2.file_name().expect("path2 must have filename"), + ).expect("can rename"); +} diff --git a/openat/src/dir.rs b/openat/src/dir.rs new file mode 100644 index 0000000..eac2f38 --- /dev/null +++ b/openat/src/dir.rs @@ -0,0 +1,696 @@ +use std::io; +use std::mem; +use std::ffi::{OsString, CStr}; +use std::fs::{File, read_link}; +use std::os::unix::io::{AsRawFd, RawFd, FromRawFd, IntoRawFd}; +use std::os::unix::ffi::{OsStringExt}; +use std::path::{PathBuf}; + +use libc; +use crate::metadata::{self, Metadata}; +use crate::list::{DirIter, open_dir, open_dirfd}; + +use crate::{Dir, AsPath}; + +#[cfg(target_os="linux")] +const BASE_OPEN_FLAGS: libc::c_int = libc::O_PATH|libc::O_CLOEXEC; +#[cfg(target_os="freebsd")] +const BASE_OPEN_FLAGS: libc::c_int = libc::O_DIRECTORY|libc::O_CLOEXEC; +#[cfg(not(any(target_os="linux", target_os="freebsd")))] +const BASE_OPEN_FLAGS: libc::c_int = libc::O_CLOEXEC; + +impl Dir { + /// Creates a directory descriptor that resolves paths relative to current + /// working directory (AT_FDCWD) + #[deprecated(since="0.1.15", note="\ + Use `Dir::open(\".\")` instead. \ + Dir::cwd() doesn't open actual file descriptor and uses magic value \ + instead which resolves to current dir on any syscall invocation. \ + This is usually counter-intuitive and yields a broken \ + file descriptor when using `Dir::as_raw_fd`. \ + Will be removed in version v0.2 of the library.")] + pub fn cwd() -> Dir { + Dir(libc::AT_FDCWD) + } + + /// Open a directory descriptor at specified path + // TODO(tailhook) maybe accept only absolute paths? + pub fn open<P: AsPath>(path: P) -> io::Result<Dir> { + Dir::_open(to_cstr(path)?.as_ref()) + } + + fn _open(path: &CStr) -> io::Result<Dir> { + let fd = unsafe { + libc::open(path.as_ptr(), BASE_OPEN_FLAGS) + }; + if fd < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(Dir(fd)) + } + } + + /// List subdirectory of this dir + /// + /// You can list directory itself with `list_self`. + pub fn list_dir<P: AsPath>(&self, path: P) -> io::Result<DirIter> { + open_dir(self, to_cstr(path)?.as_ref()) + } + + /// List this dir + pub fn list_self(&self) -> io::Result<DirIter> { + unsafe { + open_dirfd(libc::dup(self.0)) + } + } + + /// Open subdirectory + /// + /// Note that this method does not resolve symlinks by default, so you may have to call + /// [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + pub fn sub_dir<P: AsPath>(&self, path: P) -> io::Result<Dir> { + self._sub_dir(to_cstr(path)?.as_ref()) + } + + fn _sub_dir(&self, path: &CStr) -> io::Result<Dir> { + let fd = unsafe { + libc::openat(self.0, + path.as_ptr(), + BASE_OPEN_FLAGS|libc::O_NOFOLLOW) + }; + if fd < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(Dir(fd)) + } + } + + /// Read link in this directory + pub fn read_link<P: AsPath>(&self, path: P) -> io::Result<PathBuf> { + self._read_link(to_cstr(path)?.as_ref()) + } + + fn _read_link(&self, path: &CStr) -> io::Result<PathBuf> { + let mut buf = vec![0u8; 4096]; + let res = unsafe { + libc::readlinkat(self.0, + path.as_ptr(), + buf.as_mut_ptr() as *mut libc::c_char, buf.len()) + }; + if res < 0 { + Err(io::Error::last_os_error()) + } else { + buf.truncate(res as usize); + Ok(OsString::from_vec(buf).into()) + } + } + + /// Open file for reading in this directory + /// + /// Note that this method does not resolve symlinks by default, so you may have to call + /// [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + pub fn open_file<P: AsPath>(&self, path: P) -> io::Result<File> { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_RDONLY, 0) + } + + /// Open file with specified flags relative to this directory + pub fn open_file_ex<P: AsPath>(&self, path: P, flags: libc::c_int, mode: libc::mode_t) -> io::Result<File> { + self._open_file(to_cstr(path)?.as_ref(), flags, mode) + } + + /// Open file for writing, create if necessary, truncate on open + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// will need to remove the symlink before calling this method. If you are on Linux, you can + /// alternatively create an unnamed file with [`new_unnamed_file`] and then rename it, + /// clobbering the symlink at the destination. + /// + /// [`new_unnamed_file`]: #method.new_unnamed_file + pub fn write_file<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_CREAT|libc::O_WRONLY|libc::O_TRUNC, + mode) + } + + /// Open file for append, create if necessary + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// will need to call [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + pub fn append_file<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_CREAT|libc::O_WRONLY|libc::O_APPEND, + mode) + } + + /// Create file for writing (and truncate) in this directory + /// + /// Deprecated alias for `write_file` + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// will need to remove the symlink before calling this method. If you are on Linux, you can + /// alternatively create an unnamed file with [`new_unnamed_file`] and then rename it, + /// clobbering the symlink at the destination. + /// + /// [`new_unnamed_file`]: #method.new_unnamed_file + #[deprecated(since="0.1.7", note="please use `write_file` instead")] + pub fn create_file<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_CREAT|libc::O_WRONLY|libc::O_TRUNC, + mode) + } + + /// Create a tmpfile in this directory which isn't linked to any filename + /// + /// This works by passing `O_TMPFILE` into the openat call. The flag is + /// supported only on linux. So this function always returns error on + /// such systems. + /// + /// **WARNING!** On glibc < 2.22 file permissions of the newly created file + /// may be arbitrary. Consider chowning after creating a file. + /// + /// Note: It may be unclear why creating unnamed file requires a dir. There + /// are two reasons: + /// + /// 1. It's created (and occupies space) on a real filesystem, so the + /// directory is a way to find out which filesystem to attach file to + /// 2. This method is mostly needed to initialize the file then link it + /// using ``link_file_at`` to the real directory entry. When linking + /// it must be linked into the same filesystem. But because for most + /// programs finding out filesystem layout is an overkill the rule of + /// thumb is to create a file in the the target directory. + /// + /// Currently, we recommend to fallback on any error if this operation + /// can't be accomplished rather than relying on specific error codes, + /// because semantics of errors are very ugly. + #[cfg(target_os="linux")] + pub fn new_unnamed_file(&self, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(unsafe { CStr::from_bytes_with_nul_unchecked(b".\0") }, + libc::O_TMPFILE|libc::O_WRONLY, + mode) + } + + /// Create a tmpfile in this directory which isn't linked to any filename + /// + /// This works by passing `O_TMPFILE` into the openat call. The flag is + /// supported only on linux. So this function always returns error on + /// such systems. + /// + /// Note: It may be unclear why creating unnamed file requires a dir. There + /// are two reasons: + /// + /// 1. It's created (and occupies space) on a real filesystem, so the + /// directory is a way to find out which filesystem to attach file to + /// 2. This method is mostly needed to initialize the file then link it + /// using ``link_file_at`` to the real directory entry. When linking + /// it must be linked into the same filesystem. But because for most + /// programs finding out filesystem layout is an overkill the rule of + /// thumb is to create a file in the the target directory. + /// + /// Currently, we recommend to fallback on any error if this operation + /// can't be accomplished rather than relying on specific error codes, + /// because semantics of errors are very ugly. + #[cfg(not(target_os="linux"))] + pub fn new_unnamed_file<P: AsPath>(&self, _mode: libc::mode_t) + -> io::Result<File> + { + Err(io::Error::new(io::ErrorKind::Other, + "creating unnamed tmpfiles is only supported on linux")) + } + + /// Link open file to a specified path + /// + /// This is used with ``new_unnamed_file()`` to create and initialize the + /// file before linking it into a filesystem. This requires `/proc` to be + /// mounted and works **only on linux**. + /// + /// On systems other than linux this always returns error. It's expected + /// that in most cases this methos is not called if ``new_unnamed_file`` + /// fails. But in obscure scenarios where `/proc` is not mounted this + /// method may fail even on linux. So your code should be able to fallback + /// to a named file if this method fails too. + #[cfg(target_os="linux")] + pub fn link_file_at<F: AsRawFd, P: AsPath>(&self, file: &F, path: P) + -> io::Result<()> + { + let fd_path = format!("/proc/self/fd/{}", file.as_raw_fd()); + _hardlink(&Dir(libc::AT_FDCWD), to_cstr(fd_path)?.as_ref(), + &self, to_cstr(path)?.as_ref(), + libc::AT_SYMLINK_FOLLOW) + } + + /// Link open file to a specified path + /// + /// This is used with ``new_unnamed_file()`` to create and initialize the + /// file before linking it into a filesystem. This requires `/proc` to be + /// mounted and works **only on linux**. + /// + /// On systems other than linux this always returns error. It's expected + /// that in most cases this methos is not called if ``new_unnamed_file`` + /// fails. But in obscure scenarios where `/proc` is not mounted this + /// method may fail even on linux. So your code should be able to fallback + /// to a named file if this method fails too. + #[cfg(not(target_os="linux"))] + pub fn link_file_at<F: AsRawFd, P: AsPath>(&self, _file: F, _path: P) + -> io::Result<()> + { + Err(io::Error::new(io::ErrorKind::Other, + "linking unnamed fd to directories is only supported on linux")) + } + + /// Create file if not exists, fail if exists + /// + /// This function checks existence and creates file atomically with + /// respect to other threads and processes. + /// + /// Technically it means passing `O_EXCL` flag to open. + pub fn new_file<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_CREAT|libc::O_EXCL|libc::O_WRONLY, + mode) + } + + /// Open file for reading and writing without truncation, create if needed + /// + /// If there exists a symlink at the destination path, this method will fail. In that case, you + /// will need to call [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + pub fn update_file<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<File> + { + self._open_file(to_cstr(path)?.as_ref(), + libc::O_CREAT|libc::O_RDWR, + mode) + } + + fn _open_file(&self, path: &CStr, flags: libc::c_int, mode: libc::mode_t) + -> io::Result<File> + { + unsafe { + // Note: In below call to `openat`, *mode* must be cast to + // `unsigned` because the optional `mode` argument to `openat` is + // variadic in the signature. Since integers are not implicitly + // promoted as they are in C this would break on Freebsd where + // *mode_t* is an alias for `uint16_t`. + let res = libc::openat(self.0, path.as_ptr(), + flags|libc::O_CLOEXEC|libc::O_NOFOLLOW, + mode as libc::c_uint); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(File::from_raw_fd(res)) + } + } + } + + /// Make a symlink in this directory + /// + /// Note: the order of arguments differ from `symlinkat` + pub fn symlink<P: AsPath, R: AsPath>(&self, path: P, value: R) + -> io::Result<()> + { + self._symlink(to_cstr(path)?.as_ref(), to_cstr(value)?.as_ref()) + } + fn _symlink(&self, path: &CStr, link: &CStr) -> io::Result<()> { + unsafe { + let res = libc::symlinkat(link.as_ptr(), + self.0, path.as_ptr()); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + } + + /// Create a subdirectory in this directory + pub fn create_dir<P: AsPath>(&self, path: P, mode: libc::mode_t) + -> io::Result<()> + { + self._create_dir(to_cstr(path)?.as_ref(), mode) + } + fn _create_dir(&self, path: &CStr, mode: libc::mode_t) -> io::Result<()> { + unsafe { + let res = libc::mkdirat(self.0, path.as_ptr(), mode); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + } + + /// Rename a file in this directory to another name (keeping same dir) + pub fn local_rename<P: AsPath, R: AsPath>(&self, old: P, new: R) + -> io::Result<()> + { + rename(self, to_cstr(old)?.as_ref(), self, to_cstr(new)?.as_ref()) + } + + /// Similar to `local_rename` but atomically swaps both paths + /// + /// Only supported on Linux. + #[cfg(target_os="linux")] + pub fn local_exchange<P: AsPath, R: AsPath>(&self, old: P, new: R) + -> io::Result<()> + { + // Workaround https://github.com/tailhook/openat/issues/35 + // AKA https://github.com/rust-lang/libc/pull/2116 + // Unfortunately since we made this libc::c_int in our + // public API, we can't easily change it right now. + let flags = libc::RENAME_EXCHANGE as libc::c_int; + rename_flags(self, to_cstr(old)?.as_ref(), + self, to_cstr(new)?.as_ref(), + flags) + } + + /// Remove a subdirectory in this directory + /// + /// Note only empty directory may be removed + pub fn remove_dir<P: AsPath>(&self, path: P) + -> io::Result<()> + { + self._unlink(to_cstr(path)?.as_ref(), libc::AT_REMOVEDIR) + } + /// Remove a file in this directory + pub fn remove_file<P: AsPath>(&self, path: P) + -> io::Result<()> + { + self._unlink(to_cstr(path)?.as_ref(), 0) + } + fn _unlink(&self, path: &CStr, flags: libc::c_int) -> io::Result<()> { + unsafe { + let res = libc::unlinkat(self.0, path.as_ptr(), flags); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + } + + /// Get the path of this directory (if possible) + /// + /// This uses symlinks in `/proc/self`, they sometimes may not be + /// available so use with care. + pub fn recover_path(&self) -> io::Result<PathBuf> { + let fd = self.0; + if fd != libc::AT_FDCWD { + read_link(format!("/proc/self/fd/{}", fd)) + } else { + read_link("/proc/self/cwd") + } + } + + /// Returns metadata of an entry in this directory + /// + /// If the destination path is a symlink, this will return the metadata of the symlink itself. + /// If you would like to follow the symlink and return the metadata of the target, you will + /// have to call [`read_link`] to resolve the real path first. + /// + /// [`read_link`]: #method.read_link + pub fn metadata<P: AsPath>(&self, path: P) -> io::Result<Metadata> { + self._stat(to_cstr(path)?.as_ref(), libc::AT_SYMLINK_NOFOLLOW) + } + fn _stat(&self, path: &CStr, flags: libc::c_int) -> io::Result<Metadata> { + unsafe { + let mut stat = mem::zeroed(); + let res = libc::fstatat(self.0, path.as_ptr(), + &mut stat, flags); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(metadata::new(stat)) + } + } + } + + /// Returns the metadata of the directory itself. + pub fn self_metadata(&self) -> io::Result<Metadata> { + unsafe { + let mut stat = mem::zeroed(); + let res = libc::fstat(self.0, &mut stat); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(metadata::new(stat)) + } + } + } + + /// Constructs a new `Dir` from a given raw file descriptor, + /// ensuring it is a directory file descriptor first. + /// + /// This function **consumes ownership** of the specified file + /// descriptor. The returned `Dir` will take responsibility for + /// closing it when it goes out of scope. + pub unsafe fn from_raw_fd_checked(fd: RawFd) -> io::Result<Self> { + let mut stat = mem::zeroed(); + let res = libc::fstat(fd, &mut stat); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + match stat.st_mode & libc::S_IFMT { + libc::S_IFDIR => Ok(Dir(fd)), + _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)) + } + } + } + + /// Creates a new independently owned handle to the underlying directory. + pub fn try_clone(&self) -> io::Result<Self> { + let fd = unsafe { libc::dup(self.0) }; + if fd == -1 { + Err(io::Error::last_os_error()) + } else { + unsafe { Self::from_raw_fd_checked(fd) } + } + } +} + +/// Rename (move) a file between directories +/// +/// Files must be on a single filesystem anyway. This funtion does **not** +/// fallback to copying if needed. +pub fn rename<P, R>(old_dir: &Dir, old: P, new_dir: &Dir, new: R) + -> io::Result<()> + where P: AsPath, R: AsPath, +{ + _rename(old_dir, to_cstr(old)?.as_ref(), new_dir, to_cstr(new)?.as_ref()) +} + +fn _rename(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr) + -> io::Result<()> +{ + unsafe { + let res = libc::renameat(old_dir.0, old.as_ptr(), + new_dir.0, new.as_ptr()); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +/// Create a hardlink to a file +/// +/// Files must be on a single filesystem even if they are in different +/// directories. +/// +/// Note: by default ``linkat`` syscall doesn't resolve symbolic links, and +/// it's also behavior of this function. It's recommended to resolve symlinks +/// manually if needed. +pub fn hardlink<P, R>(old_dir: &Dir, old: P, new_dir: &Dir, new: R) + -> io::Result<()> + where P: AsPath, R: AsPath, +{ + _hardlink(old_dir, to_cstr(old)?.as_ref(), + new_dir, to_cstr(new)?.as_ref(), + 0) +} + +fn _hardlink(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, + flags: libc::c_int) + -> io::Result<()> +{ + unsafe { + let res = libc::linkat(old_dir.0, old.as_ptr(), + new_dir.0, new.as_ptr(), flags); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +/// Rename (move) a file between directories with flags +/// +/// Files must be on a single filesystem anyway. This funtion does **not** +/// fallback to copying if needed. +/// +/// Only supported on Linux. +#[cfg(target_os="linux")] +pub fn rename_flags<P, R>(old_dir: &Dir, old: P, new_dir: &Dir, new: R, + flags: libc::c_int) + -> io::Result<()> + where P: AsPath, R: AsPath, +{ + _rename_flags(old_dir, to_cstr(old)?.as_ref(), + new_dir, to_cstr(new)?.as_ref(), + flags) +} + +#[cfg(target_os="linux")] +fn _rename_flags(old_dir: &Dir, old: &CStr, new_dir: &Dir, new: &CStr, + flags: libc::c_int) + -> io::Result<()> +{ + unsafe { + let res = libc::syscall( + libc::SYS_renameat2, + old_dir.0, old.as_ptr(), + new_dir.0, new.as_ptr(), flags); + if res < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +impl AsRawFd for Dir { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.0 + } +} + +impl FromRawFd for Dir { + /// The user must guarantee that the passed in `RawFd` is in fact + /// a directory file descriptor. + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Dir { + Dir(fd) + } +} + +impl IntoRawFd for Dir { + #[inline] + fn into_raw_fd(self) -> RawFd { + let result = self.0; + mem::forget(self); + return result; + } +} + +impl Drop for Dir { + fn drop(&mut self) { + let fd = self.0; + if fd != libc::AT_FDCWD { + unsafe { + libc::close(fd); + } + } + } +} + +fn to_cstr<P: AsPath>(path: P) -> io::Result<P::Buffer> { + path.to_path() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, + "nul byte in file name") + }) +} + +#[cfg(test)] +mod test { + use std::io::{Read}; + use std::path::Path; + use std::os::unix::io::{FromRawFd, IntoRawFd}; + use crate::{Dir}; + + #[test] + fn test_open_ok() { + assert!(Dir::open("src").is_ok()); + } + + #[test] + #[cfg_attr(target_os="freebsd", should_panic(expected="Not a directory"))] + fn test_open_file() { + Dir::open("src/lib.rs").unwrap(); + } + + #[test] + fn test_read_file() { + let dir = Dir::open("src").unwrap(); + let mut buf = String::new(); + dir.open_file("lib.rs").unwrap() + .read_to_string(&mut buf).unwrap(); + assert!(buf.find("extern crate libc;").is_some()); + } + + #[test] + fn test_from_into() { + let dir = Dir::open("src").unwrap(); + let dir = unsafe { Dir::from_raw_fd(dir.into_raw_fd()) }; + let mut buf = String::new(); + dir.open_file("lib.rs").unwrap() + .read_to_string(&mut buf).unwrap(); + assert!(buf.find("extern crate libc;").is_some()); + } + + #[test] + #[should_panic(expected="No such file or directory")] + fn test_open_no_dir() { + Dir::open("src/some-non-existent-file").unwrap(); + } + + #[test] + fn test_list() { + let dir = Dir::open("src").unwrap(); + let me = dir.list_dir(".").unwrap(); + assert!(me.collect::<Result<Vec<_>, _>>().unwrap() + .iter().find(|x| { + x.file_name() == Path::new("lib.rs").as_os_str() + }) + .is_some()); + } + + #[test] + fn test_from_raw_fd_checked() { + let fd = Dir::open(".").unwrap().into_raw_fd(); + let dir = unsafe { Dir::from_raw_fd_checked(fd) }.unwrap(); + let filefd = dir.open_file("src/lib.rs").unwrap().into_raw_fd(); + match unsafe { Dir::from_raw_fd_checked(filefd) } { + Ok(_) => assert!(false, "from_raw_fd_checked succeeded on a non-directory fd!"), + Err(e) => assert_eq!(e.raw_os_error().unwrap(), libc::ENOTDIR) + } + } + + #[test] + fn test_try_clone() { + let d = Dir::open(".").unwrap(); + let d2 = d.try_clone().unwrap(); + drop(d); + let _file = d2.open_file("src/lib.rs").unwrap(); + } +} diff --git a/openat/src/filetype.rs b/openat/src/filetype.rs new file mode 100644 index 0000000..efaedbe --- /dev/null +++ b/openat/src/filetype.rs @@ -0,0 +1,33 @@ +use std::fs::Metadata; + +/// This is a simplified file type enum that is easy to match +/// +/// It doesn't represent all the options, because that enum needs to extensible +/// but most application do not actually need that power, so we provide +/// this simplified enum that works for many appalications. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SimpleType { + /// Entry is a symlink + Symlink, + /// Entry is a directory + Dir, + /// Entry is a regular file + File, + /// Entry is neither a symlink, directory nor a regular file + Other, +} + +impl SimpleType { + /// Find out a simple type from a file Metadata (stat) + pub fn extract(stat: &Metadata) -> SimpleType { + if stat.file_type().is_symlink() { + SimpleType::Symlink + } else if stat.is_dir() { + SimpleType::Dir + } else if stat.is_file() { + SimpleType::File + } else { + SimpleType::Other + } + } +} diff --git a/openat/src/lib.rs b/openat/src/lib.rs new file mode 100644 index 0000000..a1a2feb --- /dev/null +++ b/openat/src/lib.rs @@ -0,0 +1,95 @@ +//! # Handling Files Relative to File Descriptor +//! +//! Main concept here is a `Dir` which holds `O_PATH` file descriptor, you +//! can create it with: +//! +//! * `Dir::open("/some/path")` -- open this directory as a file descriptor +//! * `Dir::from_raw_fd(fd)` -- uses a file descriptor provided elsewhere +//! +//! *Note after opening file descriptors refer to same directory regardless of +//! where it's moved or mounted (with `pivot_root` or `mount --move`). It may +//! also be unmounted or be out of chroot and you will still be able to +//! access files relative to it.* +//! +//! *Note2: The constructor `Dir::cwd()` is deprecated, and it's recommended +//! to use `Dir::open(".")` instead.* +//! +//! *Note3: Some OS's (e.g., macOS) do not provide `O_PATH`, in which case the +//! file descriptor is of regular type.* +//! +//! Most other operations are done on `Dir` object and are executed relative +//! to it: +//! +//! * `Dir::list_dir()` +//! * `Dir::sub_dir()` +//! * `Dir::read_link()` +//! * `Dir::open_file()` +//! * `Dir::create_file()` +//! * `Dir::update_file()` +//! * `Dir::create_dir()` +//! * `Dir::symlink()` +//! * `Dir::local_rename()` +//! +//! Functions that expect path relative to the directory accept both the +//! traditional path-like objects, such as Path, PathBuf and &str, and +//! `Entry` type returned from `list_dir()`. The latter is faster as underlying +//! system call wants `CString` and we keep that in entry. +//! +//! Note that if path supplied to any method of dir is absolute the Dir file +//! descriptor is ignored. +//! +//! Also while all methods of dir accept any path if you want to prevent +//! certain symlink attacks and race condition you should only use +//! a single-component path. I.e. open one part of a chain at a time. +//! +#![warn(missing_docs)] + +extern crate libc; + +mod dir; +mod list; +mod name; +mod filetype; +mod metadata; + +pub use crate::list::DirIter; +pub use crate::name::AsPath; +pub use crate::dir::{rename, hardlink}; +pub use crate::filetype::SimpleType; +pub use crate::metadata::Metadata; + +use std::ffi::CString; +use std::os::unix::io::RawFd; + +/// A safe wrapper around directory file descriptor +/// +/// Construct it either with ``Dir::cwd()`` or ``Dir::open(path)`` +/// +#[derive(Debug)] +pub struct Dir(RawFd); + +/// Entry returned by iterating over `DirIter` iterator +#[derive(Debug)] +pub struct Entry { + name: CString, + file_type: Option<SimpleType>, +} + +#[cfg(test)] +mod test { + use std::mem; + use super::Dir; + + fn assert_sync<T: Sync>(x: T) -> T { x } + fn assert_send<T: Send>(x: T) -> T { x } + + #[test] + fn test() { + let d = Dir(3); + let d = assert_sync(d); + let d = assert_send(d); + // don't execute close for our fake RawFd + mem::forget(d); + } +} + diff --git a/openat/src/list.rs b/openat/src/list.rs new file mode 100644 index 0000000..5b4d3cd --- /dev/null +++ b/openat/src/list.rs @@ -0,0 +1,153 @@ +use std::io; +use std::ptr; +use std::ffi::{CStr, OsStr}; +use std::os::unix::ffi::OsStrExt; + +use libc; + +use crate::{Dir, Entry, SimpleType}; + + +// We have such weird constants because C types are ugly +const DOT: [libc::c_char; 2] = [b'.' as libc::c_char, 0]; +const DOTDOT: [libc::c_char; 3] = [b'.' as libc::c_char, b'.' as libc::c_char, 0]; + + +/// Iterator over directory entries +/// +/// Created using `Dir::list_dir()` +#[derive(Debug)] +pub struct DirIter { + dir: *mut libc::DIR, +} + +/// Position in a DirIter as obtained by 'DirIter::current_position()' +/// +/// The position is only valid for the DirIter it was retrieved from. +pub struct DirPosition { + pos: libc::c_long, +} + +impl Entry { + /// Returns the file name of this entry + pub fn file_name(&self) -> &OsStr { + OsStr::from_bytes(self.name.to_bytes()) + } + /// Returns the simplified type of this entry + pub fn simple_type(&self) -> Option<SimpleType> { + self.file_type + } +} + +#[cfg(any(target_os="linux", target_os="fuchsia"))] +unsafe fn errno_location() -> *mut libc::c_int { + libc::__errno_location() +} + +#[cfg(any(target_os="openbsd", target_os="netbsd", target_os="android"))] +unsafe fn errno_location() -> *mut libc::c_int { + libc::__errno() +} + +#[cfg(not(any(target_os="linux", target_os="openbsd", target_os="netbsd", target_os="android", target_os="fuchsia")))] +unsafe fn errno_location() -> *mut libc::c_int { + libc::__error() +} + +impl DirIter { + + unsafe fn next_entry(&mut self) -> io::Result<Option<&libc::dirent>> + { + // Reset errno to detect if error occurred + *errno_location() = 0; + + let entry = libc::readdir(self.dir); + if entry == ptr::null_mut() { + if *errno_location() == 0 { + return Ok(None) + } else { + return Err(io::Error::last_os_error()); + } + } + return Ok(Some(&*entry)); + } + + /// Returns the current directory iterator position. The result should be handled as opaque value + pub fn current_position(&self) -> io::Result<DirPosition> { + let pos = unsafe { libc::telldir(self.dir) }; + + if pos == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(DirPosition { pos }) + } + } + + // note the C-API does not report errors for seekdir/rewinddir, thus we don't do as well. + /// Sets the current directory iterator position to some location queried by 'current_position()' + pub fn seek(&self, position: DirPosition) { + unsafe { libc::seekdir(self.dir, position.pos) }; + } + + /// Resets the current directory iterator position to the beginning + pub fn rewind(&self) { + unsafe { libc::rewinddir(self.dir) }; + } +} + +pub fn open_dirfd(fd: libc::c_int) -> io::Result<DirIter> { + let dir = unsafe { libc::fdopendir(fd) }; + if dir == std::ptr::null_mut() { + Err(io::Error::last_os_error()) + } else { + Ok(DirIter { dir: dir }) + } +} + +pub fn open_dir(dir: &Dir, path: &CStr) -> io::Result<DirIter> { + let dir_fd = unsafe { + libc::openat(dir.0, path.as_ptr(), libc::O_DIRECTORY|libc::O_CLOEXEC) + }; + if dir_fd < 0 { + Err(io::Error::last_os_error()) + } else { + open_dirfd(dir_fd) + } +} + +impl Iterator for DirIter { + type Item = io::Result<Entry>; + fn next(&mut self) -> Option<Self::Item> { + unsafe { + loop { + match self.next_entry() { + Err(e) => return Some(Err(e)), + Ok(None) => return None, + Ok(Some(e)) if e.d_name[..2] == DOT => continue, + Ok(Some(e)) if e.d_name[..3] == DOTDOT => continue, + Ok(Some(e)) => { + return Some(Ok(Entry { + name: CStr::from_ptr((e.d_name).as_ptr()) + .to_owned(), + file_type: match e.d_type { + 0 => None, + libc::DT_REG => Some(SimpleType::File), + libc::DT_DIR => Some(SimpleType::Dir), + libc::DT_LNK => Some(SimpleType::Symlink), + _ => Some(SimpleType::Other), + }, + })); + } + } + } + } + } +} + +impl Drop for DirIter { + fn drop(&mut self) { + unsafe { + libc::closedir(self.dir); + } + } +} diff --git a/openat/src/metadata.rs b/openat/src/metadata.rs new file mode 100644 index 0000000..de7cc22 --- /dev/null +++ b/openat/src/metadata.rs @@ -0,0 +1,75 @@ +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; + +use libc; + +use crate::SimpleType; + + +/// A file metadata +/// +/// Because we can't freely create a `std::fs::Metadata` object we have to +/// implement our own structure. +pub struct Metadata { + stat: libc::stat, +} + +impl Metadata { + /// Returns simplified type of the directory entry + pub fn simple_type(&self) -> SimpleType { + let typ = self.stat.st_mode & libc::S_IFMT; + match typ { + libc::S_IFREG => SimpleType::File, + libc::S_IFDIR => SimpleType::Dir, + libc::S_IFLNK => SimpleType::Symlink, + _ => SimpleType::Other, + } + } + /// Returns underlying stat structure + pub fn stat(&self) -> &libc::stat { + &self.stat + } + /// Returns `true` if the entry is a regular file + pub fn is_file(&self) -> bool { + self.simple_type() == SimpleType::File + } + /// Returns `true` if the entry is a directory + pub fn is_dir(&self) -> bool { + self.simple_type() == SimpleType::Dir + } + /// Returns permissions of the entry + pub fn permissions(&self) -> Permissions { + Permissions::from_mode(self.stat.st_mode as u32) + } + /// Returns file size + pub fn len(&self) -> u64 { + self.stat.st_size as u64 + } +} + +pub fn new(stat: libc::stat) -> Metadata { + Metadata { stat: stat } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn dir() { + let d = crate::Dir::open(".").unwrap(); + let m = d.metadata("src").unwrap(); + assert_eq!(m.simple_type(), SimpleType::Dir); + assert!(m.is_dir()); + assert!(!m.is_file()); + } + + #[test] + fn file() { + let d = crate::Dir::open("src").unwrap(); + let m = d.metadata("lib.rs").unwrap(); + assert_eq!(m.simple_type(), SimpleType::File); + assert!(!m.is_dir()); + assert!(m.is_file()); + } +} diff --git a/openat/src/name.rs b/openat/src/name.rs new file mode 100644 index 0000000..c181db1 --- /dev/null +++ b/openat/src/name.rs @@ -0,0 +1,76 @@ +use std::ffi::{OsStr, CStr, CString}; +use std::path::{Path, PathBuf}; +use std::os::unix::ffi::OsStrExt; + +use crate::{Entry}; + + +/// The purpose of this is similar to `AsRef<Path>` but it's optimized for +/// things that can be directly used as `CStr` (which is type passed to +/// the underlying system call). +/// +/// This trait should be implemented for everything for which `AsRef<Path>` +/// is implemented +pub trait AsPath { + /// The return value of the `to_path` that holds data copied from the + /// original path (if copy is needed, otherwise it's just a reference) + type Buffer: AsRef<CStr>; + /// Returns `None` when path contains a zero byte + fn to_path(self) -> Option<Self::Buffer>; +} + +impl<'a> AsPath for &'a Path { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self.as_os_str().as_bytes()).ok() + } +} + +impl<'a> AsPath for &'a PathBuf { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self.as_os_str().as_bytes()).ok() + } +} + +impl<'a> AsPath for &'a OsStr { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self.as_bytes()).ok() + } +} + +impl<'a> AsPath for &'a str { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self.as_bytes()).ok() + } +} + +impl<'a> AsPath for &'a String { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self.as_bytes()).ok() + } +} + +impl<'a> AsPath for String { + type Buffer = CString; + fn to_path(self) -> Option<CString> { + CString::new(self).ok() + } +} + +impl<'a> AsPath for &'a CStr { + type Buffer = &'a CStr; + fn to_path(self) -> Option<&'a CStr> { + Some(self) + } +} + +impl<'a> AsPath for &'a Entry { + type Buffer = &'a CStr; + fn to_path(self) -> Option<&'a CStr> { + Some(&self.name) + } +} diff --git a/openat/tests/tmpfile.rs b/openat/tests/tmpfile.rs new file mode 100644 index 0000000..4fa0f0d --- /dev/null +++ b/openat/tests/tmpfile.rs @@ -0,0 +1,24 @@ +extern crate tempfile; +extern crate openat; + +use std::io::{self, Read, Write}; +use std::os::unix::fs::PermissionsExt; +use openat::Dir; + +#[test] +#[cfg(target_os="linux")] +fn unnamed_tmp_file_link() -> Result<(), io::Error> { + let tmp = tempfile::tempdir()?; + let dir = Dir::open(tmp.path())?; + let mut f = dir.new_unnamed_file(0o777)?; + f.write(b"hello\n")?; + // In glibc <= 2.22 permissions aren't set when using O_TMPFILE + // This includes ubuntu trusty on travis CI + f.set_permissions(PermissionsExt::from_mode(0o644))?; + dir.link_file_at(&f, "hello.txt")?; + let mut f = dir.open_file("hello.txt")?; + let mut buf = String::with_capacity(10); + f.read_to_string(&mut buf)?; + assert_eq!(buf, "hello\n"); + Ok(()) +} diff --git a/openat/vagga.yaml b/openat/vagga.yaml new file mode 100644 index 0000000..568a80d --- /dev/null +++ b/openat/vagga.yaml @@ -0,0 +1,91 @@ +commands: + + make: !Command + description: Build the library + container: ubuntu + run: [cargo, build] + + build-musl: !Command + description: Build the library with musl libc + container: ubuntu + run: [cargo, build, --target=x86_64-unknown-linux-musl] + + cargo: !Command + description: Run arbitrary cargo command + container: ubuntu + run: [cargo] + + test: !Command + description: Run tests + container: ubuntu + run: [cargo, test] + + bench: !Command + description: Run benchmarks + container: nightly + run: [cargo, bench] + + _bulk: !Command + description: Run `bulk` command (for version bookkeeping) + container: ubuntu + run: [bulk] + +containers: + + ubuntu: + setup: + - !Ubuntu xenial + - !UbuntuUniverse + - !Install [ca-certificates, git, build-essential, vim, musl-tools] + + - !TarInstall + url: "https://static.rust-lang.org/dist/rust-1.32.0-x86_64-unknown-linux-gnu.tar.gz" + script: "./install.sh --prefix=/usr \ + --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" + - !TarInstall + url: "https://static.rust-lang.org/dist/rust-std-1.32.0-x86_64-unknown-linux-musl.tar.gz" + script: "./install.sh --prefix=/musl \ + --components=rust-std-x86_64-unknown-linux-musl" + - !Sh 'ln -s /musl/lib/rustlib/x86_64-unknown-linux-musl /usr/lib/rustlib/x86_64-unknown-linux-musl' + - &bulk !Tar + url: "https://github.com/tailhook/bulk/releases/download/v0.4.10/bulk-v0.4.10.tar.gz" + sha256: 481513f8a0306a9857d045497fb5b50b50a51e9ff748909ecf7d2bda1de275ab + path: / + + environ: + HOME: /work/target + LD_LIBRARY_PATH: /musl/lib/rustlib/x86_64-unknown-linux-musl/lib + PATH: /musl/bin:/usr/local/bin:/usr/bin:/bin + RUST_BACKTRACE: 1 + + nightly: + setup: + - !Ubuntu xenial + - !Install [ca-certificates, git, build-essential] + + - !TarInstall + url: "https://static.rust-lang.org/dist/rust-nightly-x86_64-unknown-linux-gnu.tar.gz" + script: "./install.sh --prefix=/usr \ + --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" + + environ: + HOME: /work/target + RUST_BACKTRACE: 1 + + aarch64: + setup: + - !Ubuntu xenial + - !Install [ca-certificates, git, build-essential] + + - !TarInstall + url: "https://static.rust-lang.org/dist/rust-1.31.0-x86_64-unknown-linux-gnu.tar.gz" + script: "./install.sh --prefix=/usr \ + --components=rustc,cargo" + - !TarInstall + url: "https://static.rust-lang.org/dist/rust-std-1.31.0-aarch64-linux-android.tar.gz" + script: "./install.sh --prefix=/usr \ + --components=rust-std-aarch64-linux-android" + + environ: + HOME: /work/target + RUST_BACKTRACE: 1 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..23f5b79 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,56 @@ +use libc::mode_t; +use openat::{Dir, AsPath}; +use pam::constants::{PamFlag, PamResultCode}; +use pam::module::{PamHandle, PamHooks}; +use std::ffi::CStr; +use std::io::{ErrorKind, Write}; +use std::process; + +const CG_MOUNT: &str = "/sys/fs/cgroup"; + +struct PAMUserCG; +pam::pam_hooks!(PAMUserCG); + +fn create_and_open_dir<P: AsPath + Copy>( + d: &Dir, path: P, mode: mode_t, + ) -> std::io::Result<Dir> { + match d.create_dir(path, mode) { + Ok(()) => Ok(()), + Err(e) => match e.kind() { + ErrorKind::AlreadyExists => Ok(()), + _ => Err(e), + } + }?; + d.sub_dir(path) +} + +struct SessionError; + +fn open_session(h: &mut PamHandle) -> Result<(), SessionError> { + let user = h.get_user(None).or(Err(SessionError))?; + let user = users::get_user_by_name(&user).ok_or(SessionError)?; + let d = Dir::open(CG_MOUNT).or(Err(SessionError))?; + let d = create_and_open_dir(&d, "user", 0o777).or(Err(SessionError))?; + let d = create_and_open_dir(&d, &user.uid().to_string(), 0o777) + .or(Err(SessionError))?; + let d = create_and_open_dir(&d, "leaf", 0o777).or(Err(SessionError))?; + let pid = process::id().to_string(); + let mut procs = d.open_file_ex("cgroup.procs", libc::O_WRONLY, 0) + .or(Err(SessionError))?; + procs.write_all(pid.as_bytes()).or(Err(SessionError))?; + Ok(()) +} + +impl PamHooks for PAMUserCG { + fn sm_open_session( + h: &mut PamHandle, + _args: Vec<&CStr>, + _flags: PamFlag + ) -> PamResultCode { + if open_session(h).is_ok() { + PamResultCode::PAM_SUCCESS + } else { + PamResultCode::PAM_SESSION_ERR + } + } +} |