utmp-classic-0.1.6/.cargo_vcs_info.json0000644000000001360000000000100134440ustar { "git": { "sha1": "98ba8e7df49e9ec6df11290b926c1a971ebaf943" }, "path_in_vcs": "" }utmp-classic-0.1.6/.github/workflows/rust.yml000064400000000000000000000004741046102023000173560ustar 00000000000000name: Rust on: push: branches: [ "main" ] pull_request: branches: [ "main" ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose utmp-classic-0.1.6/.gitignore000064400000000000000000000006361046102023000142310ustar 00000000000000# Generated by Cargo # will have compiled files and executables debug/ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb utmp-classic-0.1.6/Cargo.lock0000644000000114670000000000100114300ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "anyhow" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27a4bd113ab6da4cd0f521068a6e2ee1065eab54107266a11835d02c8ec86a37" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "serde" version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "num-conv", "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utmp-classic" version = "0.1.6" dependencies = [ "anyhow", "cfg-if", "libc", "once_cell", "thiserror", "time", "utmp-classic-raw", "zerocopy", ] [[package]] name = "utmp-classic-raw" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22c226537a3d6e01c440c1926ca0256dbee2d19b2229ede6fc4863a6493dd831" dependencies = [ "cfg-if", "zerocopy", ] [[package]] name = "zerocopy" version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", "syn", ] utmp-classic-0.1.6/Cargo.toml0000644000000025530000000000100114470ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "utmp-classic" version = "0.1.6" authors = ["Jadi "] description = "Parsing login records in classic UNIXv1 type UTMP files; still used in OpenBSD" readme = "README.md" keywords = [ "utmp", "openbsd", "unix", "login", ] categories = ["os::unix-apis"] license = "MIT" repository = "https://github.com/jadijadi/utmp-classic" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" targets = ["aarch64-unknown-linux-gnu"] [[example]] name = "dump-utmp" path = "examples/dump_utmp.rs" [dependencies.cfg-if] version = "1.0.0" [dependencies.libc] version = "0.2.66" [dependencies.thiserror] version = "1.0.10" [dependencies.time] version = "0.3" [dependencies.utmp-classic-raw] version = "0.1.3" [dependencies.zerocopy] version = "0.7.34" [dev-dependencies.anyhow] version = "1.0.26" [dev-dependencies.once_cell] version = "1.3.1" utmp-classic-0.1.6/Cargo.toml.orig000064400000000000000000000014351046102023000151260ustar 00000000000000[package] name = "utmp-classic" edition = "2021" description = "Parsing login records in classic UNIXv1 type UTMP files; still used in OpenBSD" keywords = ["utmp", "openbsd", "unix", "login"] categories = ["os::unix-apis"] authors = ["Jadi "] repository = "https://github.com/jadijadi/utmp-classic" license = "MIT" version = "0.1.6" readme = "README.md" [[example]] name = "dump-utmp" path = "examples/dump_utmp.rs" [dependencies] cfg-if = "1.0.0" zerocopy = "0.7.34" time = "0.3" libc = "0.2.66" utmp-classic-raw = { version = "0.1.3", path = "raw" } thiserror = "1.0.10" [workspace] members = ["raw"] [dev-dependencies] anyhow = "1.0.26" once_cell = "1.3.1" [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" targets = ["aarch64-unknown-linux-gnu"] utmp-classic-0.1.6/LICENSE000064400000000000000000000020701046102023000132400ustar 00000000000000MIT License Copyright 2020 Xidorn Quan 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.utmp-classic-0.1.6/README.md000064400000000000000000000020141046102023000135100ustar 00000000000000# utmp-classic Rust library for reading utmp files. Please note that all Unix like systems (Including all GNU/Linuxes, MacOS and all BSDs except OpenBSD) use the newer `utmpx` file format, even if they still call it `utmp`. This library works only for original Unix `utmp` files which is only used in OpenBSD as far as I know. If you are looking for a lib to be used on anything other than OpenBSD; you might be looking for a `utmpx` library, although most of them calls themselves `utmp`; not sure why :D # sample run A sample `utmp` file is included in the root directory, you can run a sample by issuing: ``` cargo run --package utmp-classic --example dump-utmp utmp ``` # history This library is based on [utmp-rs](https://github.com/upsuper/utmp-rs) library by *upsuper*; changed to work on the classic AT&T Unix v1 style `utmp` files still used by OpenBSD. # rust crate - [utmp-classic crate on lib.rs](https://lib.rs/crates/utmp-classic) - [utmp-classic docs on docs.rs](https://docs.rs/utmp-classic/latest/utmp_classic/)utmp-classic-0.1.6/examples/dump_utmp.rs000064400000000000000000000010321046102023000164260ustar 00000000000000use anyhow::Result; use std::{env, process}; use std::path::PathBuf; fn main() -> Result<()> { let mut args = env::args_os(); let program_name = PathBuf::from(args.next().unwrap()); let path = match args.next() { Some(path) => PathBuf::from(path), None => { eprintln!("Usage: {} ", program_name.display()); process::exit(2); } }; let entries = utmp_classic::parse_from_path(&path)?; for entry in entries { println!("{:?}", entry); } Ok(()) } utmp-classic-0.1.6/src/entry.rs000064400000000000000000000053441046102023000145400ustar 00000000000000use std::ffi::CStr; use std::os::raw::c_short; use thiserror::Error; use time::OffsetDateTime; use utmp_classic_raw::x32::utmp as utmp32; use utmp_classic_raw::x64::{timeval as timeval64, utmp as utmp64}; /// Parsed utmp entry. #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum UtmpEntry { UTMP { /// Device name of tty line: String, /// Username user: String, /// Hostname for remote login host: String, /// Session ID (`getsid(2)`) time: OffsetDateTime, // TODO: Figure out the correct byte order to parse the address }, } impl<'a> TryFrom<&'a utmp32> for UtmpEntry { type Error = UtmpError; fn try_from(from: &utmp32) -> Result { UtmpEntry::try_from(&utmp64 { ut_line: from.ut_line, ut_user: from.ut_user, ut_host: from.ut_host, ut_tv: timeval64 { tv_sec: i64::from(from.ut_tv.tv_sec), tv_usec: i64::from(from.ut_tv.tv_usec), }, }) } } impl<'a> TryFrom<&'a utmp64> for UtmpEntry { type Error = UtmpError; fn try_from(from: &utmp64) -> Result { Ok(UtmpEntry::UTMP { line: string_from_bytes(&from.ut_line).map_err(UtmpError::InvalidLine)?, user: string_from_bytes(&from.ut_user).map_err(UtmpError::InvalidUser)?, host: string_from_bytes(&from.ut_host).map_err(UtmpError::InvalidHost)?, time: time_from_tv(from.ut_tv)?, }) } } #[derive(Debug, Error)] #[non_exhaustive] pub enum UtmpError { #[error("unknown type {0}")] UnknownType(c_short), #[error("invalid time value {0:?}")] InvalidTime(timeval64), #[error("invalid line value `{0:?}`")] InvalidLine(Box<[u8]>), #[error("invalid user value `{0:?}`")] InvalidUser(Box<[u8]>), #[error("invalid host value `{0:?}`")] InvalidHost(Box<[u8]>), } fn time_from_tv(tv: timeval64) -> Result { let timeval64 { tv_sec, tv_usec } = tv; if tv_usec < 0 { return Err(UtmpError::InvalidTime(tv)); } let usec = i128::from(tv_sec) * 1_000_000 + i128::from(tv_usec); OffsetDateTime::from_unix_timestamp_nanos(usec * 1000).map_err(|_| UtmpError::InvalidTime(tv)) } fn string_from_bytes(bytes: &[u8]) -> Result> { bytes .iter() .position(|b| *b == 0) .and_then(|pos| { // This is safe because we manually located the first zero byte above. let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=pos]) }; Some(cstr.to_str().ok()?.to_string()) }) .ok_or_else(|| bytes.to_owned().into_boxed_slice()) } utmp-classic-0.1.6/src/lib.rs000064400000000000000000000022431046102023000141400ustar 00000000000000//! A Rust crate for parsing `utmp` files like `/var/run/utmp` and `/var/log/wtmp`. //! //! ## Usage //! //! Simplest way is to use `parse_from_*` functions, //! which returns a `Vec` on success: //! ``` //! # use anyhow::Result; //! # fn main() -> Result<()> { //! let entries = utmp_classic::parse_from_path("tests/samples/basic.utmp")?; //! // ... //! # Ok(()) //! # } //! ``` //! //! If you don't need to collect them all, //! `UtmpParser` can be used as an iterator: //! ``` //! # use anyhow::Result; //! use utmp_classic::UtmpParser; //! # fn main() -> Result<()> { //! for entry in UtmpParser::from_path("tests/samples/basic.utmp")? { //! let entry = entry?; //! // ... //! } //! # Ok(()) //! # } //! ``` //! //! All the `parse_from_*` functions as well as `UtmpParser` parse `utmp` file //! based on the native format for the target platform. //! If cross-platform parsing is needed, //! `Utmp32Parser` or `Utmp64Parser` can be used instead of `UtmpParser`. mod entry; mod parse; pub use entry::{UtmpEntry, UtmpError}; pub use parse::{parse_from_file, parse_from_path, parse_from_reader}; pub use parse::{ParseError, Utmp32Parser, Utmp64Parser, UtmpParser}; utmp-classic-0.1.6/src/parse.rs000064400000000000000000000105571046102023000145130ustar 00000000000000use crate::{UtmpEntry, UtmpError}; use std::fs::File; use std::io::{self, BufReader, Read}; use std::marker::PhantomData; use std::mem; use std::path::Path; use thiserror::Error; use utmp_classic_raw::{utmp, x32::utmp as utmp32, x64::utmp as utmp64}; use zerocopy::{FromBytes, Ref}; #[doc(hidden)] pub struct UtmpParserImpl(R, PhantomData); impl UtmpParserImpl { pub fn from_reader(reader: R) -> Self { UtmpParserImpl(reader, PhantomData) } pub fn into_inner(self) -> R { self.0 } } impl UtmpParserImpl, T> { pub fn from_file(file: File) -> Self { UtmpParserImpl(BufReader::new(file), PhantomData) } pub fn from_path>(path: P) -> Result { Ok(Self::from_file(File::open(path)?)) } } /// Parser to parse a utmp file. It can be used as an iterator. /// /// ``` /// # use utmp_classic::UtmpParser; /// # fn main() -> Result<(), Box> { /// for entry in UtmpParser::from_path("tests/samples/basic.utmp")? { /// let entry = entry?; /// // handle entry /// } /// # Ok(()) /// # } /// ``` pub type UtmpParser = UtmpParserImpl; /// Parser to parse a 32-bit utmp file. pub type Utmp32Parser = UtmpParserImpl; /// Parser to parse a 64-bit utmp file. pub type Utmp64Parser = UtmpParserImpl; const UTMP32_SIZE: usize = mem::size_of::(); const UTMP64_SIZE: usize = mem::size_of::(); impl Iterator for UtmpParserImpl { type Item = Result; fn next(&mut self) -> Option { #[repr(align(4))] struct Buffer([u8; UTMP32_SIZE]); let mut buffer = Buffer([0; UTMP32_SIZE]); match read_entry::<_, utmp32>(&mut self.0, buffer.0.as_mut()) { Ok(None) => None, Ok(Some(entry)) => Some(UtmpEntry::try_from(entry).map_err(ParseError::Utmp)), Err(e) => Some(Err(e)), } } } impl Iterator for UtmpParserImpl { type Item = Result; fn next(&mut self) -> Option { #[repr(align(8))] struct Buffer([u8; UTMP64_SIZE]); let mut buffer = Buffer([0; UTMP64_SIZE]); match read_entry::<_, utmp64>(&mut self.0, buffer.0.as_mut()) { Ok(None) => None, Ok(Some(entry)) => Some(UtmpEntry::try_from(entry).map_err(ParseError::Utmp)), Err(e) => Some(Err(e)), } } } fn read_entry( mut reader: R, buffer: &mut [u8], ) -> Result, ParseError> { let size = buffer.len(); let mut buf = &mut buffer[..]; loop { match reader.read(buf) { // If the buffer has not been filled, then we just passed the last item. Ok(0) if buf.len() == size => return Ok(None), // Otherwise this is an unexpected EOF. Ok(0) => { let inner = io::Error::new(io::ErrorKind::UnexpectedEof, "size not aligned"); return Err(inner.into()); } Ok(n) => { buf = &mut buf[n..]; if buf.is_empty() { break; } } Err(e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(e.into()), } } Ok(Some( Ref::<_, T>::new(buffer).unwrap().into_ref(), )) } /// Parse utmp entries from the given path. /// /// It parses the given path using the native utmp format in the target platform. pub fn parse_from_path>(path: P) -> Result, ParseError> { UtmpParser::from_path(path)?.collect() } /// Parse utmp entries from the given file. /// /// It parses the given file using the native utmp format in the target platform. pub fn parse_from_file(file: File) -> Result, ParseError> { UtmpParser::from_file(file).collect() } /// Parse utmp entries from the given reader. /// /// It parses from the given reader using the native utmp format in the target platform. pub fn parse_from_reader(reader: R) -> Result, ParseError> { UtmpParser::from_reader(reader).collect() } #[derive(Debug, Error)] #[non_exhaustive] pub enum ParseError { #[error(transparent)] Utmp(#[from] UtmpError), #[error(transparent)] Io(#[from] io::Error), } utmp-classic-0.1.6/tests/basic.rs000064400000000000000000000023011046102023000150210ustar 00000000000000use anyhow::Result; use once_cell::sync::Lazy; use std::io::{self, Read}; use std::path::PathBuf; use time::OffsetDateTime; use utmp_classic::{parse_from_path, Utmp32Parser, Utmp64Parser, UtmpEntry}; static SAMPLES_PATH: Lazy = Lazy::new(|| PathBuf::from_iter(&[env!("CARGO_MANIFEST_DIR"), "tests", "samples"])); fn timestamp(nanos: i128) -> OffsetDateTime { OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap() } fn get_basic_expected() -> Vec { vec![ UtmpEntry::UTMP { line: "ttyC3".to_owned(), user: "jadi".to_owned(), host: "".to_owned(), time: timestamp(1714663553_000000_000), }, ] } #[test] fn parse_basic32() -> Result<()> { let path = SAMPLES_PATH.join("basic.utmp"); let actual = Utmp32Parser::from_path(&path)?.collect::, _>>()?; let expected = get_basic_expected(); Ok(assert_eq!(actual[5], expected[0])) } struct ByteReader(R); impl Read for ByteReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.len() < 1 { self.0.read(buf) } else { self.0.read(&mut buf[..1]) } } } utmp-classic-0.1.6/tests/samples/basic.utmp000064400000000000000000000034401046102023000170330ustar 00000000000000ttyC3jadi°3f