summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomasz Kramkowski <tomasz@kramkow.ski>2023-01-27 13:58:10 +0000
committerTomasz Kramkowski <tomasz@kramkow.ski>2023-01-27 13:58:10 +0000
commit6cef9f0fc159de4c9fd708050ec76adb4e74d390 (patch)
tree81856b3b4da6d18e10516b0be737a157e13f129b
parent9e8dd00da25273fba9f0cafccbde2236e04fb24e (diff)
downloadpam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.tar.gz
pam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.tar.xz
pam_usercg_rust-6cef9f0fc159de4c9fd708050ec76adb4e74d390.zip
openat variant
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml14
-rwxr-xr-xinstall2
-rw-r--r--openat/.gitignore4
-rw-r--r--openat/.travis.yml35
-rw-r--r--openat/Cargo.toml22
-rw-r--r--openat/LICENSE-APACHE202
-rw-r--r--openat/LICENSE-MIT19
-rw-r--r--openat/README.md36
-rw-r--r--openat/benches/count_processes.rs39
-rw-r--r--openat/bulk.yaml8
-rw-r--r--openat/examples/exchange.rs39
-rw-r--r--openat/src/dir.rs696
-rw-r--r--openat/src/filetype.rs33
-rw-r--r--openat/src/lib.rs95
-rw-r--r--openat/src/list.rs153
-rw-r--r--openat/src/metadata.rs75
-rw-r--r--openat/src/name.rs76
-rw-r--r--openat/tests/tmpfile.rs24
-rw-r--r--openat/vagga.yaml91
-rw-r--r--src/lib.rs56
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"
diff --git a/install b/install
new file mode 100755
index 0000000..f35979a
--- /dev/null
+++ b/install
@@ -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
+ }
+ }
+}