Skip to content
Snippets Groups Projects
Verified Commit 8f98e73a authored by Leonard Marschke's avatar Leonard Marschke :sunny:
Browse files

initial implementation

parent e1b95346
Branches
Tags
1 merge request!1initial implementation
Pipeline #25486 passed
/target
image: dr.rechenknecht.net/bauhelfer/container/main/rust:latest
stages:
- test
- build
- lint
test:
stage: test
needs: []
script:
- cargo test
# Just test generating the docs does succeed, real docs will be published once we release to crates.io/crates/gecos
- cargo doc
lint:
stage: lint
needs: []
script:
- rustup component add clippy
- cargo clippy -- -Dwarnings
- rustup component add rustfmt
- cargo fmt --all -- --check
build:
stage: build
needs: []
script:
- cargo build --release
{
"cSpell.words": [
"Gecos"
]
}
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "gecos"
version = "0.1.0"
dependencies = [
"thiserror",
]
[[package]]
name = "proc-macro2"
version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
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 = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
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 = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[package]
name = "gecos"
description = "Provides parsing and generation of gecos strings"
version = "0.1.0"
edition = "2021"
authors = ["Leonard Marschke <leo@mixxplorer.de>"]
readme = "Readme.md"
repository = "https://rechenknecht.net/mixxplorer/libraries/gecos"
license = "MIT OR Apache-2.0"
keywords = ["gecos", "nss"]
categories = ["authentication", "config", "data-structures"]
[dependencies]
thiserror = "1.0.61"
Copyright 2024 mixxplorer GmbH
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.
Copyright 2024 mixxplorer GmbH
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.
# gecos
This is a rust library to generate and parse [gecos](https://man.freebsd.org/cgi/man.cgi?query=passwd&sektion=5).
We started developing this library to be used in conjunction with [libnss](https://crates.io/crates/libnss).
For example, this library is used in the [guest-users nss package](https://rechenknecht.net/mixxplorer/guest-users/-/tree/main/nss?ref_type=heads).
## Install
Simply install via `cargo`:
```bash
cargo add gecos
```
## Usage
For a full reference, please check out the [`Gecos`] struct.
```rust
use std::convert::TryFrom;
use gecos::{Gecos, GecosSanitizedString};
// read gecos string from passwd etc.
let raw_gecos_string = "Some Person,Room,Work phone,Home phone,Other 1,Other 2";
let mut gecos = Gecos::from_gecos_string(raw_gecos_string).unwrap();
// access fields like
// var field option for comp
assert_eq!(gecos.full_name.as_ref().unwrap().to_string(), "Some Person");
// and you even can convert it back to a raw gecos string
assert_eq!(gecos.to_gecos_string(), raw_gecos_string);
// modifying fields work like this
gecos.full_name = Some("Another name".to_string().try_into().unwrap());
// or more explicitly
gecos.room = Some(GecosSanitizedString::new("St. 9".to_string()).unwrap());
assert_eq!(gecos.full_name.as_ref().unwrap().to_string(), "Another name");
assert_eq!(gecos.room.as_ref().unwrap().to_string(), "St. 9");
```
#![warn(missing_docs)]
#![deny(warnings)]
#![deny(clippy::all)]
#![doc = include_str!("../Readme.md")]
use thiserror::Error;
/// Error type for gecos errors. All public facing Results will carry this error type.
#[derive(Error, Debug)]
pub enum GecosError {
/// Illegal character for passwd representations
#[error("String contains invalid char, which is not allowed inside a Gecos field! (Chars ',', ':', '=', '\\', '\"', '\\n' are not allowed)")]
IllegalPasswdChar(char),
}
/// The raw Gecos struct.
///
/// See [the man page](https://man.freebsd.org/cgi/man.cgi?query=passwd&sektion=5) for an introduction of the format.
///
/// To set most of the fields, you need to assign a [GecosSanitizedString] in order to ensure the strings do not contain ',', ':', '=', '\', '"', '\n'
/// This is done in compatibility with [chfn](https://github.com/util-linux/util-linux/blob/0284eb3a8a6505dd9745b042089046ad368bfe74/login-utils/chfn.c#L121C6-L121C26).
///
/// ## Usage
///
/// You can create new gecos object and also parse existing ones from their string representation.
///
/// ## Creating gecos
///
/// Every field except other can be None, if there is no value. When creating an object, you can do this explicitly:
///
/// ```rust
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos {
/// full_name: None,
/// room: None,
/// work_phone: None,
/// home_phone: None,
/// other: vec![],
/// };
///
/// // the most simple outcome, everything is just empty,
/// // therefore the `to_gecos_string` function produces only ','.
/// assert_eq!(gecos.to_gecos_string(), ",,,,")
/// ```
///
/// Of course, you can also set some data, which looks like this. Please note the special case of the `other` field.
/// As some implementations allow multiple "other" fields, you can pass a vector. It is your responsibility to ensure compatibility here.
///
/// ```rust
/// # use std::convert::TryFrom;
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos {
/// full_name: Some("Test Name".to_string().try_into().unwrap()),
/// room: None,
/// work_phone: None,
/// home_phone: None,
/// other: vec![
/// "Some info".to_string().try_into().unwrap(),
/// "More info".to_string().try_into().unwrap()
/// ],
/// };
///
/// assert_eq!(gecos.to_gecos_string(), "Test Name,,,,Some info,More info")
/// ```
///
/// Utilizing [`Gecos::to_gecos_string`] allows converting a [`Gecos`] object to a gecos string like you find it in the passwd database.
///
/// ## Parse gecos
///
/// You can parse gecos objects by calling the [`Gecos::from_gecos_string`] static function of this object. A simple example might look like this:
///
/// ```rust
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos::from_gecos_string("Some Person,,,,").unwrap();
///
/// assert_eq!(gecos.full_name.unwrap().to_string(), "Some Person")
/// ```
///
#[derive(Clone, Debug)]
pub struct Gecos {
/// like Guest, can be None if empty.
pub full_name: Option<GecosSanitizedString>,
/// like H-1.13, can be None if empty.
pub room: Option<GecosSanitizedString>,
/// like 574, can be None if empty.
pub work_phone: Option<GecosSanitizedString>,
/// like +491606799999, can be None if empty.
pub home_phone: Option<GecosSanitizedString>,
/// like a mail address or other important information, vector can be empty if there is no data.
pub other: Vec<GecosSanitizedString>,
}
/// A struct to ensure the string has none of [',', ':', '=', '\', '"', '\n'] in it, as this would break the gecos string object.
///
/// You can create a new object by converting a String into a GecosSanitizesString like this:
/// ```rust
/// # use std::convert::TryFrom;
/// # use gecos::GecosSanitizedString;
/// #
/// // simple example (type is typically inferred)
/// let name_gecos: GecosSanitizedString = "Another name".to_string().try_into().unwrap();
/// // or more explicitly
/// let room_gecos = GecosSanitizedString::new("St. 9".to_string()).unwrap();
///
/// // converting to standard String
/// let name_string = name_gecos.to_string();
/// ```
#[derive(Clone, Debug)]
pub struct GecosSanitizedString {
str: String,
}
impl GecosSanitizedString {
/// Returns a new [GecosSanitizedString] object.
pub fn new(value: String) -> Result<Self, GecosError> {
const INVALID_CHARS: [&char; 6] = [&',', &':', &'=', &'\\', &'\"', &'\n'];
for character in INVALID_CHARS {
if value.contains(*character) {
return Err(GecosError::IllegalPasswdChar(*character));
}
}
Ok(Self { str: value })
}
}
impl TryFrom<String> for GecosSanitizedString {
type Error = GecosError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<'a> From<&'a GecosSanitizedString> for &'a String {
fn from(value: &'a GecosSanitizedString) -> Self {
&value.str
}
}
impl std::fmt::Display for GecosSanitizedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.str)
}
}
impl PartialEq for GecosSanitizedString {
fn eq(&self, other: &Self) -> bool {
self.str == other.str
}
}
impl Gecos {
/// Converts a [Gecos] object to a gecos string like in the passwd database.
///
/// ```rust
/// # use std::convert::TryFrom;
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos {
/// full_name: Some("Test Name".to_string().try_into().unwrap()),
/// room: None,
/// work_phone: None,
/// home_phone: None,
/// other: vec![],
/// };
///
/// assert_eq!(gecos.to_gecos_string(), "Test Name,,,,")
/// ```
pub fn to_gecos_string(&self) -> String {
macro_rules! gecos_element_to_string {
($sts:expr) => {
$sts.as_ref().unwrap_or(&"".to_string().try_into().unwrap())
};
}
format!(
"{},{},{},{},{}",
gecos_element_to_string!(self.full_name),
gecos_element_to_string!(self.room),
gecos_element_to_string!(self.work_phone),
gecos_element_to_string!(self.home_phone),
self.other
.iter()
.map(|val| val.into())
.cloned()
.collect::<Vec<String>>()
.join(","),
)
}
/// Converts a gecos string like in passwd database into a [Gecos] object.
///
/// ```rust
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos::from_gecos_string("Some Person,Room,Work phone,Home phone,Other 1,Other 2").unwrap();
///
/// assert_eq!(gecos.full_name.unwrap().to_string(), "Some Person");
/// assert_eq!(gecos.room.unwrap().to_string(), "Room");
/// assert_eq!(gecos.work_phone.unwrap().to_string(), "Work phone");
/// assert_eq!(gecos.home_phone.unwrap().to_string(), "Home phone");
/// assert_eq!(gecos.other.iter().map(|val| val.to_string()).collect::<Vec<String>>(), ["Other 1", "Other 2"]);
/// ```
///
/// Also support parsing strings, which do not have all fields populated:
///
///```rust
/// # use gecos::Gecos;
/// #
/// let gecos = Gecos::from_gecos_string("Some Person,,,Home phone,Other").unwrap();
///
/// assert_eq!(gecos.full_name.unwrap().to_string(), "Some Person");
/// assert!(gecos.room.is_none());
/// assert!(gecos.work_phone.is_none());
/// assert_eq!(gecos.home_phone.unwrap().to_string(), "Home phone");
/// assert_eq!(gecos.other.iter().map(|val| val.to_string()).collect::<Vec<String>>(), ["Other"]);
/// ```
///
/// or even incomplete
///
/// ```rust
/// # use gecos::{Gecos, GecosSanitizedString};
/// #
/// let gecos = Gecos::from_gecos_string("Some Person").unwrap();
///
/// assert_eq!(gecos.full_name.unwrap().to_string(), "Some Person");
/// assert!(gecos.room.is_none());
/// assert!(gecos.work_phone.is_none());
/// assert!(gecos.home_phone.is_none());
/// assert_eq!(gecos.other, Vec::<GecosSanitizedString>::new());
/// ```
pub fn from_gecos_string(input: &str) -> Result<Self, GecosError> {
let mut splitted = input
.split(',')
.map(|val| -> Result<GecosSanitizedString, GecosError> { val.to_string().try_into() });
macro_rules! gecos_string_element_to_gecos_object_element {
($sts:expr) => {
match $sts {
Some(option_val) => {
match option_val {
Ok(val) => {
// map empty string to None as well
if val.to_string() == "" {
None
} else {
Some(val)
}
}
Err(err) => return Err(err),
}
}
None => None,
}
};
}
Ok(Self {
full_name: gecos_string_element_to_gecos_object_element!(splitted.next()),
room: gecos_string_element_to_gecos_object_element!(splitted.next()),
work_phone: gecos_string_element_to_gecos_object_element!(splitted.next()),
home_phone: gecos_string_element_to_gecos_object_element!(splitted.next()),
other: splitted.collect::<Result<Vec<GecosSanitizedString>, GecosError>>()?,
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment