51 Commits

Author SHA1 Message Date
John Turner
b753519a3e add parse method to Parseable trait for easy parsing 2025-11-30 22:47:12 +00:00
John Turner
abf784a784 add some docs 2025-11-30 22:12:49 +00:00
John Turner
13a6ab5d21 set buildtype to debugoptimized 2025-11-30 19:05:19 +00:00
John Turner
f06859c447 run meson compile before running check commands 2025-11-30 19:05:02 +00:00
John Turner
b0311ba813 use lld not ldd 2025-11-30 18:49:49 +00:00
John Turner
94f3397d19 use fs.stem instead of name in tests/meson.build 2025-11-29 16:43:10 +00:00
John Turner
f0ffe5cb2b add porthole test to check_commands.txt 2025-11-28 17:23:15 +00:00
John Turner
327d871c16 add repo parser test 2025-11-28 17:22:16 +00:00
John Turner
7b60034425 use gnu parallel to run pre-commit hook check commands in parallel 2025-11-28 17:16:33 +00:00
John Turner
558e213ab4 add porthole tests to meson 2025-11-28 17:13:47 +00:00
John Turner
ee5b3c8166 bump mon 2025-11-26 19:24:45 +00:00
John Turner
86e2b4559a derive PartialEq and Eq for Atom and Atom related types 2025-11-23 05:18:30 +00:00
John Turner
5be1e5c37a derive Hash for Atom and similar types 2025-11-23 03:30:37 +00:00
John Turner
f8149b43d4 rearrange modules 2025-11-23 02:49:53 +00:00
John Turner
bffc1e88b0 allow "0" as a build-id 2025-11-23 01:34:01 +00:00
John Turner
ffa1a05fc1 remove invalid comment 2025-11-23 01:33:07 +00:00
John Turner
ac1eb15ea7 enable fuzz feature in check.sh 2025-11-22 05:52:53 +00:00
John Turner
de9fd0fbd9 print the remaining input on fuzzer failures 2025-11-22 05:49:45 +00:00
John Turner
9062881692 disallow wildcard after a build-id 2025-11-22 01:20:55 +00:00
John Turner
e9603ce62f represent 4th variant of slots, and disallow empty primary slot names 2025-11-22 01:03:14 +00:00
John Turner
bd0fec80f9 verify that repo names are also valid package names 2025-11-22 00:17:14 +00:00
John Turner
c06360aed6 disallow "+" in repo names 2025-11-22 00:16:09 +00:00
John Turner
64065b753b remove unneeded targets from Cargo.toml 2025-11-21 23:23:57 +00:00
John Turner
9eaf25f8c8 change "test" meson option to "tests" 2025-11-21 23:21:15 +00:00
John Turner
fb69d82e6f build-id must not start with zero 2025-11-21 04:33:55 +00:00
John Turner
bf56ed1c61 remove build-id from control version and reject inputs with it 2025-11-21 02:25:22 +00:00
John Turner
3bce987993 format version build-id 2025-11-20 23:50:47 +00:00
John Turner
360a44d608 port check.sh to use only meson 2025-11-20 23:49:46 +00:00
John Turner
699d4bafd0 update mon and use new ascii parsers 2025-11-20 23:27:41 +00:00
John Turner
ff7d9b312f fix lints 2025-11-19 05:04:44 +00:00
John Turner
ad8a4b838b remove dbg! 2025-11-19 05:04:44 +00:00
John Turner
0d40608404 compare versions as strings rather than parsing them to ints
Parsing version numbers to u64s could cause an panic on int overflow
with very large versions.
2025-11-19 05:04:40 +00:00
John Turner
8d3cf7c83d allow missing panic docs 2025-11-19 05:01:12 +00:00
John Turner
16fdd27e9a compare letters before suffixes, and having a letter is greater than none 2025-11-19 01:17:18 +00:00
John Turner
70e8ea24a8 impl vercmp fuzzer 2025-11-19 01:00:48 +00:00
John Turner
e01637fd3a setup meson to allow building multiple fuzzers easily 2025-11-18 22:43:22 +00:00
John Turner
e0cc7f6a03 dont allow "." in repo names 2025-11-18 04:24:41 +00:00
John Turner
c75a38f615 allow slot to be only :* := :slot/sub= or :slot 2025-11-18 04:15:53 +00:00
John Turner
2dc5df6112 support portage build-id extension 2025-11-18 03:21:44 +00:00
John Turner
e2cc948803 take at least 1 usedep 2025-11-18 02:49:30 +00:00
John Turner
920ec36141 skip atoms that portage denies for having duplicate usedeps 2025-11-18 02:46:59 +00:00
John Turner
2d0a91eb18 check if fuzz input is graphical before decoding it to UTF8 2025-11-18 02:46:33 +00:00
John Turner
46c3c075d1 disallow atoms that end in what could be a valid version 2025-11-18 02:20:57 +00:00
John Turner
78398b7ebe support ::repo syntax 2025-11-18 01:44:45 +00:00
John Turner
db02762ee1 version wildcard comes after the version expression 2025-11-18 01:27:29 +00:00
John Turner
d4fd6cd211 add false positive case to unit tests 2025-11-17 22:58:46 +00:00
John Turner
34362dcb29 in fuzz python process, inherit stderr so we can see python errors 2025-11-17 22:46:09 +00:00
John Turner
dc47258841 reject fuzz inputs with invisible characters 2025-11-17 22:45:41 +00:00
John Turner
63db65b2f0 bump mon for bug fixes 2025-11-17 21:20:40 +00:00
John Turner
b74471706b communicate with python over a pipe to increase fuzzing performance 2025-11-17 20:02:16 +00:00
John Turner
0cc3ac8e84 verify that slot exprs have either a primary slot name or operator 2025-11-17 19:01:39 +00:00
31 changed files with 877 additions and 425 deletions

2
Cargo.lock generated
View File

@@ -40,7 +40,7 @@ dependencies = [
[[package]] [[package]]
name = "mon" name = "mon"
version = "0.1.0" version = "0.1.0"
source = "git+https://jturnerusa.dev/cgit/mon/?rev=34d8eeb989012b0f20041b11a60ced24ca702527#34d8eeb989012b0f20041b11a60ced24ca702527" source = "git+https://jturnerusa.dev/cgit/mon/?rev=67861a4df8a5abdd70651d47cf265b20c41d2acc#67861a4df8a5abdd70651d47cf265b20c41d2acc"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"

View File

@@ -4,18 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
mon = { git = "https://jturnerusa.dev/cgit/mon/", rev = "34d8eeb989012b0f20041b11a60ced24ca702527" } mon = { git = "https://jturnerusa.dev/cgit/mon/", rev = "67861a4df8a5abdd70651d47cf265b20c41d2acc" }
get = { git = "https://jturnerusa.dev/cgit/get/", rev = "cd5f75b65777a855ab010c3137304ac05f2e56b8" } get = { git = "https://jturnerusa.dev/cgit/get/", rev = "cd5f75b65777a855ab010c3137304ac05f2e56b8" }
itertools = "0.14.0" itertools = "0.14.0"
thiserror = "2.0.17" thiserror = "2.0.17"
[profile.dev]
debug = true
[[bin]]
path = "fuzz/gencorpus.rs"
name = "gencorpus"
[[test]]
path = "fuzz/fuzz.rs"
name = "fuzz"

View File

@@ -1,26 +1,22 @@
#!/bin/bash #!/bin/bash
source /etc/profile source /etc/profile
source /lib/gentoo/functions.sh
export CC=clang CXX=clang++ export PATH="${HOME}/.local/bin:${PATH}" CC=clang CXX=clang++
cargo fmt --check || exit $? lld=$(command -v lld)
cargo clippy || exit $? if [[ -n ${ldd} ]]; then
export LDFLAGS=-fuse-ld=${lld}
fi
cargo test -r || exit $? if [[ ! -d build ]]; then
meson setup -Dfuzz=enabled -Dtests=enabled -Dbuildtype=debugoptimized -Ddocs=enabled build || exit $?
fi
cargo build --all --all-features || exit $? meson compile -C build || exit $?
build=$(mktemp -d) ebegin "running check commands"
parallel --halt soon,fail=1 --keep-order -j$(nproc) < check_commands.txt
meson setup ${build} || exit $? eend $? || exit $?
meson compile -C ${build} || exit $?
meson test -C ${build} || exit $?
rm -rf ${build}
# hack to make sure we use the system meson, since meson format from git is broken
/usr/bin/meson format --recursive --check-only || exit $?

5
check_commands.txt Normal file
View File

@@ -0,0 +1,5 @@
/usr/bin/meson format --recursive --check-only
rustfmt --edition 2024 --check $(find src -type f -name '*.rs')
ninja rustdoc -C build
ninja clippy -C build
meson test unittests doctests '*repo*' '*porthole*' -C build

2
fuzz/atom/meson.build Normal file
View File

@@ -0,0 +1,2 @@
subdir('parser')
subdir('vercmp')

100
fuzz/atom/parser/fuzz.rs Normal file
View File

@@ -0,0 +1,100 @@
use core::slice;
use gentoo_utils::{Parseable, atom::Atom};
use mon::{Parser, ParserFinishedError, input::InputIter};
use std::{
io::{BufRead, BufReader, Write},
process::{ChildStdin, ChildStdout, Command, Stdio},
sync::{LazyLock, Mutex},
};
struct PyProcess {
stdin: Mutex<ChildStdin>,
stdout: Mutex<BufReader<ChildStdout>>,
buffer: Mutex<String>,
}
#[allow(clippy::missing_safety_doc, clippy::needless_return)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn LLVMFuzzerTestOneInput(input: *const u8, len: usize) -> i32 {
static PY_PROCESS: LazyLock<PyProcess> = LazyLock::new(|| {
#[allow(clippy::zombie_processes)]
let mut proc = Command::new("atom.py")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("failed to spawn atom.py");
let stdin = Mutex::new(proc.stdin.take().unwrap());
let stdout = Mutex::new(BufReader::new(proc.stdout.take().unwrap()));
PyProcess {
stdin,
stdout,
buffer: Mutex::new(String::new()),
}
});
let slice = unsafe { slice::from_raw_parts(input, len) };
if slice.iter().any(|b| !b.is_ascii_graphic()) {
return -1;
}
let str = match str::from_utf8(slice) {
Ok(str) => str,
Err(_) => return -1,
};
let atom = str.trim();
let mut stdin = PY_PROCESS.stdin.lock().expect("failed to get stdin lock");
writeln!(&mut stdin, "{atom}").expect("failed to write to python stdin");
let mut stdout = PY_PROCESS.stdout.lock().expect("failed to get stdout lock");
let mut buffer = PY_PROCESS.buffer.lock().expect("failed to get buffer lock");
buffer.clear();
stdout
.read_line(&mut buffer)
.expect("failed to readline from python");
let portage_result = match buffer.as_str().trim() {
"0" => true,
"1" => false,
result => panic!("got unexpected result from python: {result}"),
};
let gentoo_utils_result = Atom::parser().parse_finished(InputIter::new(atom));
match (portage_result, gentoo_utils_result) {
(true, Ok(_)) => {
eprintln!("agreement that {atom} is valid");
}
(false, Err(_)) => {
eprintln!("agreement that {atom} is invalid");
}
(true, Err(ParserFinishedError::Err(it) | ParserFinishedError::Unfinished(it))) => {
panic!("rejected valid atom: {atom}: {}", it.rest());
}
(false, Ok(atom))
if atom.usedeps().iter().any(|usedep| {
atom.usedeps()
.iter()
.filter(|u| usedep.flag() == u.flag())
.count()
> 1
}) =>
{
eprintln!("disagreement due to duplicates in usedeps");
}
(false, Ok(_)) => {
panic!("accpeted invalid atom: {atom}")
}
}
return 0;
}

View File

@@ -8,7 +8,7 @@ use std::{
use gentoo_utils::{ use gentoo_utils::{
atom::Atom, atom::Atom,
ebuild::{Depend, repo::Repo}, repo::{Repo, ebuild::Depend},
}; };
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {

View File

@@ -0,0 +1,6 @@
fuzzers += {
'atom_parser': [
meson.current_source_dir() / 'gencorpus.rs',
meson.current_source_dir() / 'fuzz.rs',
],
}

114
fuzz/atom/vercmp/fuzz.rs Normal file
View File

@@ -0,0 +1,114 @@
use core::slice;
use gentoo_utils::{
Parseable,
atom::{Atom, Version},
};
use mon::{Parser, ParserFinishedError, input::InputIter};
use std::{
cmp::Ordering,
io::{BufRead, BufReader, Write},
process::{ChildStdin, ChildStdout, Command, Stdio},
sync::{LazyLock, Mutex},
};
struct PyProcess {
stdin: Mutex<ChildStdin>,
stdout: Mutex<BufReader<ChildStdout>>,
buffer: Mutex<String>,
}
#[allow(clippy::missing_safety_doc, clippy::needless_return)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn LLVMFuzzerTestOneInput(input: *const u8, len: usize) -> i32 {
static PY_PROCESS: LazyLock<PyProcess> = LazyLock::new(|| {
#[allow(clippy::zombie_processes)]
let mut proc = Command::new("vercmp.py")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("failed to spawn vercmp.py");
let stdin = Mutex::new(proc.stdin.take().unwrap());
let stdout = Mutex::new(BufReader::new(proc.stdout.take().unwrap()));
PyProcess {
stdin,
stdout,
buffer: Mutex::new(String::new()),
}
});
let control = Version::parser()
.parse_finished(InputIter::new("1.2.0a_alpha1_beta2-r1"))
.unwrap();
let slice = unsafe { slice::from_raw_parts(input, len) };
if slice.iter().any(|b| !b.is_ascii_graphic()) {
return -1;
}
let str = match str::from_utf8(slice) {
Ok(str) => str,
Err(_) => return -1,
};
let version_str = match str.split_ascii_whitespace().next() {
Some(lhs) => lhs,
None => return -1,
};
let version = match Version::parser().parse_finished(InputIter::new(version_str)) {
Ok(a) => a,
Err(_) => return -1,
};
if version.build_id().is_some() {
return -1;
}
let gentoo_utils = control.cmp(&version);
let portage_result = portage_vercmp(&PY_PROCESS, &control, &version);
match portage_result {
Ok(portage) if portage == gentoo_utils => {
eprintln!("agreement on {control} cmp {version} == {portage:?}");
}
Ok(portage) => {
panic!(
"disagreement on {control} == {version}:\nportage:{portage:?} gentoo_utils:{gentoo_utils:?}"
)
}
Err(_) => {
panic!("parsed invalid versions: {control} | {version}")
}
}
return 0;
}
fn portage_vercmp(pyproc: &PyProcess, a: &Version, b: &Version) -> Result<Ordering, ()> {
let mut stdin = pyproc.stdin.lock().expect("failed to get stdin lock");
let mut stdout = pyproc.stdout.lock().expect("failed to get stdout lock");
let mut buffer = pyproc.buffer.lock().expect("failed to get buffer lock");
writeln!(&mut stdin, "{a} {b}").expect("failed to write line to python process");
stdin.flush().unwrap();
buffer.clear();
stdout
.read_line(&mut buffer)
.expect("failed to read line from python process");
match buffer.as_str().trim() {
"0" => Ok(Ordering::Equal),
"1" => Ok(Ordering::Greater),
"-1" => Ok(Ordering::Less),
"err" => Err(()),
other => panic!("unexpected result from python: {other}"),
}
}

View File

@@ -0,0 +1,43 @@
use std::{
env,
error::Error,
fs::{self, OpenOptions},
io::Write,
path::PathBuf,
};
use gentoo_utils::repo::Repo;
fn main() -> Result<(), Box<dyn Error>> {
let corpus_dir = PathBuf::from(
env::args()
.nth(1)
.expect("expected corpus directory as first argument"),
);
fs::create_dir_all(&corpus_dir)?;
let repo = Repo::new("/var/db/repos/gentoo");
let mut versions = Vec::new();
for category in repo.categories()? {
for ebuild in category?.ebuilds()? {
let version = ebuild?.version().clone();
versions.push(version);
}
}
for (i, version) in versions.iter().enumerate() {
let path = corpus_dir.as_path().join(i.to_string());
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)?;
write!(file, "{version}")?;
}
Ok(())
}

View File

@@ -0,0 +1,6 @@
fuzzers += {
'atom_vercmp': [
meson.current_source_dir() / 'gencorpus.rs',
meson.current_source_dir() / 'fuzz.rs',
],
}

View File

@@ -1,50 +0,0 @@
use core::slice;
use gentoo_utils::{Parseable, atom::Atom};
use mon::{Parser, ParserFinishedError, input::InputIter};
use std::{
io::Write,
process::{Command, Stdio},
};
#[allow(clippy::missing_safety_doc, clippy::needless_return)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn LLVMFuzzerTestOneInput(input: *const u8, len: usize) -> i32 {
let slice = unsafe { slice::from_raw_parts(input, len) };
let atom = match str::from_utf8(slice) {
Ok(str) => str.trim(),
_ => return -1,
};
let mut proc = Command::new("atom.py")
.stdin(Stdio::piped())
.spawn()
.expect("failed to start atom.py");
proc.stdin
.as_mut()
.unwrap()
.write_all(atom.as_bytes())
.unwrap();
let status = proc.wait().unwrap();
let result = Atom::parser().check_finished(InputIter::new(atom));
match (status.success(), result) {
(true, Ok(_)) => {
eprintln!("agreement that {atom} is valid");
return 0;
}
(true, Err(ParserFinishedError::Err(it) | ParserFinishedError::Unfinished(it))) => {
panic!("gentoo-utils rejected valid atom: {atom}: {}", it.rest());
}
(false, Err(_)) => {
eprintln!("agreement that {atom} is invalid");
return -1;
}
(false, Ok(_)) => {
panic!("gentoo-utils accepted invalid atom: {atom}");
}
}
}

View File

@@ -1,30 +1,38 @@
cbindgen = find_program('cbindgen') cbindgen = find_program('cbindgen')
gencorpus = executable( fuzzers = {}
'gencorpus',
'gencorpus.rs', subdir('atom')
foreach fuzzer, sources : fuzzers
gencorpus_rs = sources[0]
fuzz_rs = sources[1]
gencorpus = executable(
fuzzer + '_' + 'gencorpus',
gencorpus_rs,
dependencies: [mon], dependencies: [mon],
link_with: [gentoo_utils], link_with: [gentoo_utils],
) )
corpus_directory = meson.current_build_dir() / 'corpus' corpus_directory = fuzzer + '_' + 'corpus'
corpus = custom_target( corpus = custom_target(
'corpus', fuzzer + '_' + 'corpus',
output: 'corpus', output: fuzzer + '_' + 'corpus',
command: [gencorpus, corpus_directory], command: [gencorpus, corpus_directory],
) )
fuzz_h = custom_target( fuzz_h = custom_target(
'fuzz_h', fuzzer + '_' + 'fuzz_h',
input: 'fuzz.rs', input: fuzz_rs,
output: 'fuzz.h', output: fuzzer + '_' + 'fuzz.h',
command: [cbindgen, '@INPUT@', '-o', '@OUTPUT'], command: [cbindgen, '@INPUT@', '-o', '@OUTPUT'],
) )
fuzz_rs = static_library( fuzz_rs = static_library(
'fuzz_rs', fuzzer + '.rs',
'fuzz.rs', fuzz_rs,
rust_abi: 'c', rust_abi: 'c',
rust_args: [ rust_args: [
'-Cpasses=sancov-module', '-Cpasses=sancov-module',
@@ -33,8 +41,19 @@ fuzz_rs = static_library(
], ],
dependencies: [mon], dependencies: [mon],
link_with: [gentoo_utils], link_with: [gentoo_utils],
) )
fuzz = executable('fuzz', link_args: ['-fsanitize=fuzzer'], link_with: [fuzz_rs]) fuzz = executable(
fuzzer + '_' + 'fuzzer',
link_args: ['-fsanitize=fuzzer'],
link_with: [fuzz_rs],
)
test('fuzz', fuzz, args: [corpus_directory], depends: [corpus], timeout: 0) test(
fuzzer + '_' + 'fuzz',
fuzz,
args: [corpus_directory],
depends: [corpus],
timeout: 0,
)
endforeach

View File

@@ -20,6 +20,21 @@ gentoo_utils = static_library(
link_with: [thiserror], link_with: [thiserror],
) )
if get_option('tests').enabled()
rust.test('unittests', gentoo_utils)
subdir('tests')
endif
if get_option('fuzz').enabled() if get_option('fuzz').enabled()
subdir('fuzz') subdir('fuzz')
endif endif
if get_option('docs').enabled()
rust.doctest(
'doctests',
gentoo_utils,
dependencies: [mon, get, itertools],
link_with: [thiserror],
args: ['--nocapture'],
)
endif

View File

@@ -1 +1,3 @@
option('fuzz', type: 'feature', value: 'disabled') option('fuzz', type: 'feature', value: 'disabled')
option('tests', type: 'feature', value: 'disabled')
option('docs', type: 'feature', value: 'disabled')

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
import portage.dep as dep from portage.dep import Atom
input = sys.stdin.read().strip() for line in sys.stdin.buffer:
try:
try: Atom(line.decode().strip())
dep.Atom(input) sys.stdout.buffer.write(b"0\n")
except Exception as e: except:
sys.exit(1) sys.stdout.buffer.write(b"1\n")
finally:
sys.exit(0) sys.stdout.buffer.flush()

14
scripts/vercmp.py Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
import sys
import portage
for line in sys.stdin.buffer:
a, b = line.decode().split(" ")
try:
sys.stdout.buffer.write(f"{portage.vercmp(a, b)}\n".encode())
except:
sys.stdout.buffer.write(b"err\n")
finally:
sys.stdout.buffer.flush()

View File

@@ -10,15 +10,15 @@ use get::Get;
use itertools::Itertools; use itertools::Itertools;
pub mod parsers; mod parsers;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Blocker { pub enum Blocker {
Weak, Weak,
Strong, Strong,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum VersionOperator { pub enum VersionOperator {
Lt, Lt,
Gt, Gt,
@@ -28,19 +28,19 @@ pub enum VersionOperator {
Roughly, Roughly,
} }
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct Category(#[get(method = "get", kind = "deref")] String); pub struct Category(#[get(method = "get", kind = "deref")] String);
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct Name(#[get(method = "get", kind = "deref")] String); pub struct Name(#[get(method = "get", kind = "deref")] String);
#[derive(Clone, Debug, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct VersionNumber(#[get(method = "get", kind = "deref")] String); pub struct VersionNumber(#[get(method = "get", kind = "deref")] String);
#[derive(Debug, Clone, Get)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Get)]
struct VersionNumbers(#[get(method = "get", kind = "deref")] Vec<VersionNumber>); struct VersionNumbers(#[get(method = "get", kind = "deref")] Vec<VersionNumber>);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VersionSuffixKind { pub enum VersionSuffixKind {
Alpha, Alpha,
Beta, Beta,
@@ -49,58 +49,75 @@ pub enum VersionSuffixKind {
P, P,
} }
#[derive(Clone, Debug, Get)] #[derive(Clone, Debug, Hash, PartialEq, Eq, Get)]
pub struct VersionSuffix { pub struct VersionSuffix {
kind: VersionSuffixKind, kind: VersionSuffixKind,
number: Option<VersionNumber>, number: Option<VersionNumber>,
} }
#[derive(Debug, Clone, Get)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Get)]
pub struct VersionSuffixes(#[get(method = "get", kind = "deref")] Vec<VersionSuffix>); pub struct VersionSuffixes(#[get(method = "get", kind = "deref")] Vec<VersionSuffix>);
#[derive(Clone, Debug, Get)] #[derive(Debug, Clone, Get, PartialEq, Eq, Hash)]
pub struct BuildId(#[get(method = "get", kind = "deref")] String);
#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct Version { pub struct Version {
numbers: VersionNumbers, numbers: VersionNumbers,
letter: Option<char>, letter: Option<char>,
suffixes: VersionSuffixes, suffixes: VersionSuffixes,
rev: Option<VersionNumber>, rev: Option<VersionNumber>,
build_id: Option<BuildId>,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Wildcard;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SlotOperator { pub enum SlotOperator {
Eq, Eq,
Star, Star,
} }
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct SlotName(#[get(method = "name", kind = "deref")] String); pub struct SlotName(#[get(method = "name", kind = "deref")] String);
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Slot { pub enum Slot {
primary: Option<SlotName>, Wildcard,
Equal,
NameEqual {
primary: SlotName,
sub: Option<SlotName>, sub: Option<SlotName>,
operator: Option<SlotOperator>, },
Name {
primary: SlotName,
sub: Option<SlotName>,
},
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UseDepNegate { pub enum UseDepNegate {
Minus, Minus,
Exclamation, Exclamation,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UseDepSign { pub enum UseDepSign {
Enabled, Enabled,
Disabled, Disabled,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UseDepCondition { pub enum UseDepCondition {
Eq, Eq,
Question, Question,
} }
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Repo(String);
#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct UseDep { pub struct UseDep {
negate: Option<UseDepNegate>, negate: Option<UseDepNegate>,
flag: UseFlag, flag: UseFlag,
@@ -108,13 +125,13 @@ pub struct UseDep {
condition: Option<UseDepCondition>, condition: Option<UseDepCondition>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct Cp { pub struct Cp {
category: Category, category: Category,
name: Name, name: Name,
} }
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct Cpv { pub struct Cpv {
category: Category, category: Category,
name: Name, name: Name,
@@ -122,13 +139,14 @@ pub struct Cpv {
slot: Option<Slot>, slot: Option<Slot>,
} }
#[derive(Clone, Debug, Get, PartialEq, Eq)] #[derive(Clone, Debug, Get, PartialEq, Eq, Hash)]
pub struct Atom { pub struct Atom {
blocker: Option<Blocker>, blocker: Option<Blocker>,
category: Category, category: Category,
name: Name, name: Name,
version: Option<(VersionOperator, Version)>, version: Option<(VersionOperator, Version, Option<Wildcard>)>,
slot: Option<Slot>, slot: Option<Slot>,
repo: Option<Repo>,
#[get(kind = "deref")] #[get(kind = "deref")]
usedeps: Vec<UseDep>, usedeps: Vec<UseDep>,
} }
@@ -146,7 +164,7 @@ impl Cpv {
impl Atom { impl Atom {
#[must_use] #[must_use]
pub fn version_operator(&self) -> Option<VersionOperator> { pub fn version_operator(&self) -> Option<VersionOperator> {
self.version.clone().map(|(oper, _)| oper) self.version.clone().map(|(oper, _, _)| oper)
} }
#[must_use] #[must_use]
@@ -160,7 +178,7 @@ impl Atom {
#[must_use] #[must_use]
pub fn into_cpv(self) -> Option<Cpv> { pub fn into_cpv(self) -> Option<Cpv> {
match self.version { match self.version {
Some((_, version)) => Some(Cpv { Some((_, version, _)) => Some(Cpv {
category: self.category, category: self.category,
name: self.name, name: self.name,
version, version,
@@ -171,20 +189,43 @@ impl Atom {
} }
} }
impl PartialEq for VersionSuffix { impl VersionNumber {
fn eq(&self, other: &Self) -> bool { #[must_use]
self.kind == other.kind pub fn cmp_as_ints(&self, other: &Self) -> Ordering {
&& match (&self.number, &other.number) { let a = self.get().trim_start_matches('0');
(Some(a), Some(b)) => a.0 == b.0, let b = other.get().trim_start_matches('0');
(Some(a), None) if a.get().chars().all(|c| c == '0') => true,
(None, Some(b)) if b.get().chars().all(|c| c == '0') => true, a.len().cmp(&b.len()).then_with(|| a.cmp(b))
(Some(_), None) | (None, Some(_)) => false, }
(None, None) => true,
#[must_use]
pub fn cmp_as_str(&self, other: &Self) -> Ordering {
if self.get().starts_with('0') || other.get().starts_with('0') {
let a = self.get().trim_end_matches('0');
let b = other.get().trim_end_matches('0');
a.cmp(b)
} else {
self.cmp_as_ints(other)
} }
} }
} }
impl Eq for VersionSuffix {} impl PartialOrd for BuildId {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BuildId {
fn cmp(&self, other: &Self) -> Ordering {
// build-id may not start with a zero so we dont need to strip them
self.get()
.len()
.cmp(&other.get().len())
.then_with(|| self.get().cmp(other.get()))
}
}
impl PartialOrd for VersionSuffix { impl PartialOrd for VersionSuffix {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
@@ -198,11 +239,7 @@ impl Ord for VersionSuffix {
Ordering::Less => Ordering::Less, Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater, Ordering::Greater => Ordering::Greater,
Ordering::Equal => match (&self.number, &other.number) { Ordering::Equal => match (&self.number, &other.number) {
(Some(a), Some(b)) => { (Some(a), Some(b)) => a.cmp_as_ints(b),
a.0.parse::<u64>()
.unwrap()
.cmp(&b.0.parse::<u64>().unwrap())
}
(Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal, (Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal,
(None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal, (None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal,
(Some(_), None) => Ordering::Greater, (Some(_), None) => Ordering::Greater,
@@ -213,23 +250,6 @@ impl Ord for VersionSuffix {
} }
} }
impl PartialEq for VersionSuffixes {
fn eq(&self, other: &Self) -> bool {
let mut a = self.get().iter();
let mut b = other.get().iter();
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) if a == b => (),
(Some(_) | None, Some(_)) | (Some(_), None) => break false,
(None, None) => break true,
}
}
}
}
impl Eq for VersionSuffixes {}
impl PartialOrd for VersionSuffixes { impl PartialOrd for VersionSuffixes {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@@ -260,41 +280,6 @@ impl Ord for VersionSuffixes {
} }
} }
impl PartialEq for VersionNumbers {
fn eq(&self, other: &Self) -> bool {
self.get().first().unwrap().get().parse::<u64>().unwrap()
== other.get().first().unwrap().get().parse().unwrap()
&& {
let mut a = self.get().iter().skip(1);
let mut b = other.get().iter().skip(1);
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) if a.get().starts_with('0') => {
let a = a.get().trim_end_matches('0');
let b = b.get().trim_end_matches('0');
if a != b {
break false;
}
}
(Some(a), Some(b)) => {
if a.get().parse::<u64>().unwrap() != b.get().parse::<u64>().unwrap() {
break false;
}
}
(Some(a), None) if a.get().chars().all(|c| c == '0') => (),
(None, Some(b)) if b.get().chars().all(|c| c == '0') => (),
(None, None) => break true,
_ => break false,
}
}
}
}
}
impl Eq for VersionNumbers {}
impl PartialOrd for VersionNumbers { impl PartialOrd for VersionNumbers {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@@ -307,10 +292,7 @@ impl Ord for VersionNumbers {
.get() .get()
.first() .first()
.unwrap() .unwrap()
.get() .cmp_as_ints(other.get().first().unwrap())
.parse::<u64>()
.unwrap()
.cmp(&other.get().first().unwrap().get().parse::<u64>().unwrap())
{ {
Ordering::Less => Ordering::Less, Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater, Ordering::Greater => Ordering::Greater,
@@ -320,24 +302,7 @@ impl Ord for VersionNumbers {
loop { loop {
match (a.next(), b.next()) { match (a.next(), b.next()) {
(Some(a), Some(b)) (Some(a), Some(b)) => match a.cmp_as_str(b) {
if a.get().starts_with('0') || b.get().starts_with('0') =>
{
let a = a.get().trim_end_matches('0');
let b = b.get().trim_end_matches('0');
match a.cmp(b) {
Ordering::Less => break Ordering::Less,
Ordering::Greater => break Ordering::Greater,
Ordering::Equal => (),
}
}
(Some(a), Some(b)) => match a
.get()
.parse::<u64>()
.unwrap()
.cmp(&b.get().parse::<u64>().unwrap())
{
Ordering::Less => break Ordering::Less, Ordering::Less => break Ordering::Less,
Ordering::Greater => break Ordering::Greater, Ordering::Greater => break Ordering::Greater,
Ordering::Equal => (), Ordering::Equal => (),
@@ -353,25 +318,6 @@ impl Ord for VersionNumbers {
} }
} }
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
self.numbers == other.numbers
&& self.suffixes == other.suffixes
&& self.letter == other.letter
&& match (&self.rev, &other.rev) {
(Some(a), Some(b)) => {
a.get().parse::<u64>().unwrap() == b.get().parse::<u64>().unwrap()
}
(Some(a), None) if a.get().chars().all(|c| c == '0') => true,
(None, Some(b)) if b.get().chars().all(|c| c == '0') => true,
(Some(_), None) | (None, Some(_)) => false,
(None, None) => true,
}
}
}
impl Eq for Version {}
impl PartialOrd for Version { impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@@ -386,12 +332,6 @@ impl Ord for Version {
Ordering::Equal => (), Ordering::Equal => (),
} }
match self.suffixes.cmp(&other.suffixes) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
Ordering::Equal => (),
}
match (self.letter, other.letter) { match (self.letter, other.letter) {
(Some(a), Some(b)) if a < b => return Ordering::Less, (Some(a), Some(b)) if a < b => return Ordering::Less,
(Some(a), Some(b)) if a > b => return Ordering::Greater, (Some(a), Some(b)) if a > b => return Ordering::Greater,
@@ -402,15 +342,28 @@ impl Ord for Version {
_ => unreachable!(), _ => unreachable!(),
} }
match self.suffixes.cmp(&other.suffixes) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
Ordering::Equal => (),
}
match (&self.rev, &other.rev) { match (&self.rev, &other.rev) {
(Some(a), Some(b)) => a (Some(a), Some(b)) => match a.cmp_as_ints(b) {
.get() Ordering::Less => return Ordering::Less,
.parse::<u64>() Ordering::Greater => return Ordering::Greater,
.unwrap() Ordering::Equal => (),
.cmp(&b.get().parse().unwrap()), },
(Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal, (Some(a), None) if a.get().chars().all(|c| c == '0') => (),
(Some(_), None) => return Ordering::Greater,
(None, Some(b)) if b.get().chars().all(|c| c == '0') => (),
(None, Some(_)) => return Ordering::Less,
(None, None) => (),
}
match (&self.build_id, &other.build_id) {
(Some(a), Some(b)) => a.cmp(b),
(Some(_), None) => Ordering::Greater, (Some(_), None) => Ordering::Greater,
(None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal,
(None, Some(_)) => Ordering::Less, (None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal, (None, None) => Ordering::Equal,
} }
@@ -467,6 +420,12 @@ impl fmt::Display for VersionNumber {
} }
} }
impl fmt::Display for BuildId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.get())
}
}
impl fmt::Display for VersionSuffixKind { impl fmt::Display for VersionSuffixKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -523,6 +482,10 @@ impl fmt::Display for Version {
write!(f, "-r{rev}")?; write!(f, "-r{rev}")?;
} }
if let Some(build_id) = self.build_id.as_ref() {
write!(f, "-{build_id}")?;
}
Ok(()) Ok(())
} }
} }
@@ -544,20 +507,31 @@ impl fmt::Display for SlotName {
impl fmt::Display for Slot { impl fmt::Display for Slot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(slot) = self.primary.as_ref() { match self {
write!(f, "{slot}")?; Self::Wildcard => write!(f, "*"),
Self::Equal => {
write!(f, "=")
} }
Self::NameEqual { primary, sub } => {
write!(f, "{primary}")?;
if let Some(sub) = self.sub.as_ref() { if let Some(sub) = sub {
write!(f, "/{sub}")?; write!(f, "/{sub}")?;
} }
if let Some(operator) = self.operator.as_ref() { write!(f, "=")
write!(f, "{operator}")?; }
Self::Name { primary, sub } => {
write!(f, "{primary}")?;
if let Some(sub) = sub {
write!(f, "/{sub}")?;
} }
Ok(()) Ok(())
} }
}
}
} }
impl fmt::Display for UseDepNegate { impl fmt::Display for UseDepNegate {
@@ -639,8 +613,10 @@ impl fmt::Display for Atom {
write!(f, "/")?; write!(f, "/")?;
write!(f, "{}", self.name)?; write!(f, "{}", self.name)?;
if let Some((_, version)) = self.version.as_ref() { if let Some((_, version, None)) = self.version() {
write!(f, "-{version}")?; write!(f, "-{version}")?;
} else if let Some((_, version, Some(_))) = self.version() {
write!(f, "-{version}*")?;
} }
if let Some(slot) = self.slot.as_ref() { if let Some(slot) = self.slot.as_ref() {
@@ -710,38 +686,6 @@ mod test {
assert_eq!(atom.to_string().as_str(), s); assert_eq!(atom.to_string().as_str(), s);
} }
#[test]
fn test_version_suffix_eq() {
let a = VersionSuffix::parser()
.parse_finished(InputIter::new("alpha0"))
.unwrap();
let b = VersionSuffix::parser()
.parse_finished(InputIter::new("alpha"))
.unwrap();
assert_eq_display!(a, b);
}
#[test]
fn test_version_eq() {
let versions = [
("1", "1"),
("1", "1.0.0"),
("1.0", "1.0.0"),
("1.0.0_alpha0", "1.0.0_alpha"),
("1.0.0", "1.0.0-r0"),
];
for (a, b) in versions.map(|(a, b)| {
(
Version::parser().parse_finished(InputIter::new(a)).unwrap(),
Version::parser().parse_finished(InputIter::new(b)).unwrap(),
)
}) {
assert_eq_display!(a, b);
}
}
#[test] #[test]
fn test_version_cmp() { fn test_version_cmp() {
let versions = [ let versions = [
@@ -750,6 +694,7 @@ mod test {
("1.0.0_alpha", "1.0.0_alpha_p", Ordering::Less), ("1.0.0_alpha", "1.0.0_alpha_p", Ordering::Less),
("1.0.0-r0", "1.0.0", Ordering::Equal), ("1.0.0-r0", "1.0.0", Ordering::Equal),
("1.0.0-r0000", "1.0.0", Ordering::Equal), ("1.0.0-r0000", "1.0.0", Ordering::Equal),
("1.0.0-r1-1", "1.0.0-r1-2", Ordering::Less),
]; ];
for (a, b, ordering) in versions.iter().map(|(a, b, ordering)| { for (a, b, ordering) in versions.iter().map(|(a, b, ordering)| {
@@ -816,4 +761,19 @@ mod test {
assert_cmp_display!(a, b, Ordering::Greater); assert_cmp_display!(a, b, Ordering::Greater);
} }
#[test]
fn test_fuzzer_cases() {
let control = Version::parser()
.parse_finished(InputIter::new("1.2.0a_alpha1_beta2-r1-8"))
.unwrap();
for (version_str, expected) in [("1.2.0", Ordering::Greater)] {
let version = Version::parser()
.parse_finished(InputIter::new(version_str))
.unwrap();
assert_cmp_display!(control, version, expected);
}
}
} }

View File

@@ -1,13 +1,16 @@
use core::option::Option::None; use core::option::Option::None;
use mon::{Parser, ParserIter, alphanumeric, r#if, numeric1, one_of, tag}; use mon::{
Parser, ParserIter, ascii_alphanumeric, ascii_numeric, ascii_numeric1, eof, r#if,
input::InputIter, one_of, tag,
};
use crate::{ use crate::{
Parseable, Parseable,
atom::{ atom::{
Atom, Blocker, Category, Cp, Cpv, Name, Slot, SlotName, SlotOperator, UseDep, Atom, Blocker, BuildId, Category, Cp, Cpv, Name, Repo, Slot, SlotName, SlotOperator,
UseDepCondition, UseDepNegate, UseDepSign, Version, VersionNumber, VersionNumbers, UseDep, UseDepCondition, UseDepNegate, UseDepSign, Version, VersionNumber, VersionNumbers,
VersionOperator, VersionSuffix, VersionSuffixKind, VersionSuffixes, VersionOperator, VersionSuffix, VersionSuffixKind, VersionSuffixes, Wildcard,
}, },
useflag::UseFlag, useflag::UseFlag,
}; };
@@ -40,7 +43,22 @@ impl<'a> Parseable<'a, &'a str> for VersionNumber {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
numeric1().map(|output: &str| VersionNumber(output.to_string())) ascii_numeric1().map(|output: &str| VersionNumber(output.to_string()))
}
}
impl<'a> Parseable<'a, &'a str> for BuildId {
type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser {
let start = ascii_numeric().and_not(tag("0"));
let rest = ascii_numeric().repeated().many();
start
.and(rest)
.recognize()
.or(tag("0"))
.map(|output: &str| BuildId(output.to_string()))
} }
} }
@@ -72,9 +90,6 @@ impl<'a> Parseable<'a, &'a str> for VersionNumbers {
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
VersionNumber::parser() VersionNumber::parser()
.followed_by(tag("*").opt())
.recognize()
.map(|output: &str| VersionNumber(output.to_string()))
.separated_by(tag(".")) .separated_by(tag("."))
.at_least(1) .at_least(1)
.map(VersionNumbers) .map(VersionNumbers)
@@ -97,16 +112,19 @@ impl<'a> Parseable<'a, &'a str> for Version {
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let rev = VersionNumber::parser().preceded_by(tag("-r")); let rev = VersionNumber::parser().preceded_by(tag("-r"));
let build_id = BuildId::parser().preceded_by(tag("-"));
VersionNumbers::parser() VersionNumbers::parser()
.and(r#if(|c: &char| c.is_ascii_alphabetic() && c.is_ascii_lowercase()).opt()) .and(r#if(|c: &char| c.is_ascii_alphabetic() && c.is_ascii_lowercase()).opt())
.and(VersionSuffixes::parser().preceded_by(tag("_")).opt()) .and(VersionSuffixes::parser().preceded_by(tag("_")).opt())
.and(rev.opt()) .and(rev.opt())
.map(|(((numbers, letter), suffixes), rev)| Version { .and(build_id.opt())
.map(|((((numbers, letter), suffixes), rev), build_id)| Version {
numbers, numbers,
letter, letter,
suffixes: suffixes.unwrap_or(VersionSuffixes(Vec::new())), suffixes: suffixes.unwrap_or(VersionSuffixes(Vec::new())),
rev, rev,
build_id,
}) })
} }
} }
@@ -115,8 +133,11 @@ impl<'a> Parseable<'a, &'a str> for Category {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric().or(one_of("_".chars())); let start = ascii_alphanumeric().or(one_of("_".chars()));
let rest = alphanumeric().or(one_of("+_.-".chars())).repeated().many(); let rest = ascii_alphanumeric()
.or(one_of("+_.-".chars()))
.repeated()
.many();
start start
.and(rest) .and(rest)
@@ -129,21 +150,31 @@ impl<'a> Parseable<'a, &'a str> for Name {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric().or(one_of("_".chars())); let start = || ascii_alphanumeric().or(one_of("_".chars()));
let rest = alphanumeric() let rest = ascii_alphanumeric()
.or(one_of("_+".chars())) .or(one_of("_+".chars()))
.or(one_of("-".chars()).and_not( .or(one_of("-".chars()).and_not(
Version::parser() Version::parser()
.preceded_by(tag("-")) .preceded_by(tag("-"))
.followed_by(alphanumeric().or(one_of("_+-".chars())).not()), .followed_by(ascii_alphanumeric().or(one_of("_+-".chars())).not()),
)) ))
.repeated() .repeated()
.many(); .many();
start let verify = ascii_alphanumeric()
.or(one_of("_+".chars()))
.or(one_of("-".chars())
.and_not(Version::parser().preceded_by(tag("-")).followed_by(eof())))
.repeated()
.many();
start()
.and(rest) .and(rest)
.recognize() .recognize()
.verify_output(move |output: &&str| {
verify.check_finished(InputIter::new(*output)).is_ok()
})
.map(|output: &str| Name(output.to_string())) .map(|output: &str| Name(output.to_string()))
} }
} }
@@ -162,8 +193,11 @@ impl<'a> Parseable<'a, &'a str> for SlotName {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric().or(one_of("_".chars())); let start = ascii_alphanumeric().or(one_of("_".chars()));
let rest = alphanumeric().or(one_of("+_.-".chars())).repeated().many(); let rest = ascii_alphanumeric()
.or(one_of("+_.-".chars()))
.repeated()
.many();
start start
.and(rest) .and(rest)
@@ -176,15 +210,17 @@ impl<'a> Parseable<'a, &'a str> for Slot {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
SlotName::parser() let wildcard = tag("*").map(|_| Slot::Wildcard);
.opt() let equal = tag("=").map(|_| Slot::Equal);
let name_equal = SlotName::parser()
.and(SlotName::parser().preceded_by(tag("/")).opt()) .and(SlotName::parser().preceded_by(tag("/")).opt())
.and(SlotOperator::parser().opt()) .followed_by(tag("="))
.map(|((primary, sub), operator)| Slot { .map(|(primary, sub)| Slot::NameEqual { primary, sub });
primary, let name = SlotName::parser()
sub, .and(SlotName::parser().preceded_by(tag("/")).opt())
operator, .map(|(primary, sub)| Self::Name { primary, sub });
})
wildcard.or(equal).or(name_equal).or(name)
} }
} }
@@ -198,6 +234,28 @@ impl<'a> Parseable<'a, &'a str> for UseDepSign {
} }
} }
impl<'a> Parseable<'a, &'a str> for Repo {
type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser {
let start = ascii_alphanumeric().or(one_of("_".chars()));
let rest = ascii_alphanumeric()
.or(one_of("_-".chars()))
.repeated()
.many();
start
.and(rest)
.recognize()
.verify_output(move |output: &&str| {
Name::parser()
.check_finished(InputIter::new(*output))
.is_ok()
})
.map(|output: &str| Repo(output.to_string()))
}
}
impl<'a> Parseable<'a, &'a str> for UseDep { impl<'a> Parseable<'a, &'a str> for UseDep {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
@@ -275,7 +333,7 @@ impl<'a> Parseable<'a, &'a str> for Atom {
let usedeps = || { let usedeps = || {
UseDep::parser() UseDep::parser()
.separated_by(tag(",")) .separated_by(tag(","))
.many() .at_least(1)
.delimited_by(tag("["), tag("]")) .delimited_by(tag("["), tag("]"))
.opt() .opt()
}; };
@@ -285,15 +343,19 @@ impl<'a> Parseable<'a, &'a str> for Atom {
.and(Category::parser()) .and(Category::parser())
.and(Name::parser().preceded_by(tag("/"))) .and(Name::parser().preceded_by(tag("/")))
.and(Slot::parser().preceded_by(tag(":")).opt()) .and(Slot::parser().preceded_by(tag(":")).opt())
.and(Repo::parser().preceded_by(tag("::")).opt())
.and(usedeps()) .and(usedeps())
.map(|((((blocker, category), name), slot), usedeps)| Atom { .map(
|(((((blocker, category), name), slot), repo), usedeps)| Atom {
blocker, blocker,
category, category,
name, name,
version: None, version: None,
slot, slot,
repo,
usedeps: usedeps.unwrap_or(Vec::new()), usedeps: usedeps.unwrap_or(Vec::new()),
}); },
);
let with_version = Blocker::parser() let with_version = Blocker::parser()
.opt() .opt()
@@ -301,33 +363,37 @@ impl<'a> Parseable<'a, &'a str> for Atom {
.and(Category::parser()) .and(Category::parser())
.and(Name::parser().preceded_by(tag("/"))) .and(Name::parser().preceded_by(tag("/")))
.and(Version::parser().preceded_by(tag("-"))) .and(Version::parser().preceded_by(tag("-")))
.and(tag("*").map(|_| Wildcard).opt())
.and(Slot::parser().preceded_by(tag(":")).opt()) .and(Slot::parser().preceded_by(tag(":")).opt())
.and(Repo::parser().preceded_by(tag("::")).opt())
.and(usedeps()) .and(usedeps())
.verify_output(
|((((((((_, version_operator), _), _), version), star), _), _), _)| {
matches!(
(version_operator, star),
(VersionOperator::Eq, Some(_) | None) | (_, None)
) && matches!((version.build_id(), star), (Some(_), None) | (None, _))
},
)
.map( .map(
|((((((blocker, version_operator), category), name), version), slot), usedeps)| { |(
(
((((((blocker, version_operator), category), name), version), star), slot),
repo,
),
usedeps,
)| {
Atom { Atom {
blocker, blocker,
category, category,
name, name,
version: Some((version_operator, version)), version: Some((version_operator, version, star)),
slot, slot,
repo,
usedeps: usedeps.unwrap_or(Vec::new()), usedeps: usedeps.unwrap_or(Vec::new()),
} }
}, },
) );
.verify_output(|atom| match &atom.version {
Some((VersionOperator::Eq, _)) => true,
Some((_, version))
if !version
.numbers()
.get()
.iter()
.any(|number| number.get().contains('*')) =>
{
true
}
_ => false,
});
with_version.or(without_version) with_version.or(without_version)
} }
@@ -420,9 +486,9 @@ mod test {
#[test] #[test]
fn test_empty_slot() { fn test_empty_slot() {
let it = InputIter::new("foo/bar:="); let it = InputIter::new("=dev-ml/uucp-17*:");
Atom::parser().check_finished(it).unwrap(); assert!(Atom::parser().check_finished(it).is_err());
} }
#[test] #[test]
@@ -494,4 +560,40 @@ mod test {
assert!(Cpv::parser().parse_finished(it).is_err()); assert!(Cpv::parser().parse_finished(it).is_err());
} }
#[test]
fn test_empty_slot_with_operator() {
let it = InputIter::new("foo/bar:=");
Atom::parser().check_finished(it).unwrap();
}
#[test]
fn test_with_repo() {
let it = InputIter::new("=foo/bar-1.0.0:slot/sub=::gentoo[a,b,c]");
Atom::parser().check_finished(it).unwrap();
}
#[test]
fn test_against_fuzzer_false_positives() {
let atoms = [
"media-libs/libsdl2[haptitick(+),sound(+)vd,eio(+)]",
"=kde-frameworks/kcodecs-6.19*86",
"=dev-ml/stdio-0.17*t:=[ocamlopt?]",
">=dev-libs/libgee-0-8.5:0..8=",
"<dev-haskell/wai-3.3:=[]",
">=kde-frameworks/kcrash-2.16.0:6*",
"0-f/merreka+m::k+",
"iev-a/h:/n=",
"=dev-ml/stdio-0-17*:=[ocamlopt?]",
];
for atom in atoms {
assert!(
Atom::parser().check_finished(InputIter::new(atom)).is_err(),
"{atom}"
);
}
}
} }

View File

@@ -1,15 +1,91 @@
//! Gentoo and PMS related utils.
//!
//! Currently implements:
//! - parsers for atoms and DEPEND expressions
//! - strongly typed representations of atoms, versions, etc
//! - version comparison and equality impls
//! - iterator over repos categories and ebuilds
//!
//! Planned features
//! - profile evaluation
//! - vdb reader
//! - sourcing ebuilds with bash
//!
#![deny(clippy::pedantic, unused_imports)] #![deny(clippy::pedantic, unused_imports)]
#![allow(dead_code, unstable_name_collisions, clippy::missing_errors_doc)] #![allow(
dead_code,
unstable_name_collisions,
clippy::missing_errors_doc,
clippy::missing_panics_doc
)]
#![feature(impl_trait_in_assoc_type)] #![feature(impl_trait_in_assoc_type)]
use mon::{Parser, input::Input}; use mon::{
Parser,
input::{Input, InputIter},
};
pub trait Parseable<'a, I: Input + 'a> { pub trait Parseable<'a, I: Input + 'a> {
type Parser: Parser<I, Output = Self>; type Parser: Parser<I, Output = Self>;
fn parser() -> Self::Parser; fn parser() -> Self::Parser;
fn parse(input: I) -> Result<Self, I>
where
Self: Sized,
{
Self::parser()
.parse_finished(InputIter::new(input))
.map_err(|e| e.rest())
}
} }
/// Strongly typed atom and cpv representations.
///
/// Create atoms from parsers:
/// ```
/// use gentoo_utils::{Parseable, atom::Atom};
///
/// let emacs = Atom::parse("=app-editors/emacs-31.0-r1")
/// .expect("failed to parse atom");
///
/// assert_eq!(emacs.to_string(), "=app-editors/emacs-31.0-r1");
/// ````
///
/// Compare versions:
/// ```
/// use gentoo_utils::{Parseable, atom::Cpv};
///
/// let a = Cpv::parse("foo/bar-1.0").unwrap();
/// let b = Cpv::parse("foo/bar-2.0").unwrap();
///
/// assert!(a < b);
/// ```
pub mod atom; pub mod atom;
pub mod ebuild;
/// Access to repos and ebuilds.
///
/// ```
/// use gentoo_utils::repo::Repo;
///
/// let repo = Repo::new("/var/db/repos/gentoo");
///
/// for result in repo.categories().expect("failed to read categories") {
/// let category = result.expect("failed to read category");
///
/// for result in category.ebuilds().expect("failed to read ebuilds") {
/// let ebuild = result.expect("failed to read ebuild");
///
/// println!(
/// "{}-{}: {}",
/// ebuild.name(),
/// ebuild.version(),
/// ebuild.description().clone().unwrap_or("no description available".to_string())
/// );
/// }
/// }
///
/// ```
pub mod repo;
pub mod useflag; pub mod useflag;

View File

@@ -6,8 +6,7 @@ use crate::{
useflag::{IUseFlag, UseFlag}, useflag::{IUseFlag, UseFlag},
}; };
pub mod parsers; mod parsers;
pub mod repo;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Conditional { pub enum Conditional {
@@ -59,26 +58,26 @@ pub struct Eclass(#[get(method = "get", kind = "deref")] String);
#[derive(Debug, Clone, Get)] #[derive(Debug, Clone, Get)]
pub struct Ebuild { pub struct Ebuild {
name: Name, pub(super) name: Name,
version: Version, pub(super) version: Version,
slot: Option<Slot>, pub(super) slot: Option<Slot>,
homepage: Option<String>, pub(super) homepage: Option<String>,
#[get(kind = "deref")] #[get(kind = "deref")]
src_uri: Vec<Depend<SrcUri>>, pub(super) src_uri: Vec<Depend<SrcUri>>,
eapi: Option<Eapi>, pub(super) eapi: Option<Eapi>,
#[get(kind = "deref")] #[get(kind = "deref")]
inherit: Vec<Eclass>, pub(super) inherit: Vec<Eclass>,
#[get(kind = "deref")] #[get(kind = "deref")]
iuse: Vec<IUseFlag>, pub(super) iuse: Vec<IUseFlag>,
#[get(kind = "deref")] #[get(kind = "deref")]
license: Vec<Depend<License>>, pub(super) license: Vec<Depend<License>>,
description: Option<String>, pub(super) description: Option<String>,
#[get(kind = "deref")] #[get(kind = "deref")]
depend: Vec<Depend<Atom>>, pub(super) depend: Vec<Depend<Atom>>,
#[get(kind = "deref")] #[get(kind = "deref")]
bdepend: Vec<Depend<Atom>>, pub(super) bdepend: Vec<Depend<Atom>>,
#[get(kind = "deref")] #[get(kind = "deref")]
rdepend: Vec<Depend<Atom>>, pub(super) rdepend: Vec<Depend<Atom>>,
#[get(kind = "deref")] #[get(kind = "deref")]
idepend: Vec<Depend<Atom>>, pub(super) idepend: Vec<Depend<Atom>>,
} }

View File

@@ -1,10 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use mon::{Parser, ParserIter, alpha1, alphanumeric, r#if, one_of, tag, whitespace1}; use mon::{
Parser, ParserIter, ascii_alpha1, ascii_alphanumeric, ascii_whitespace1, r#if, one_of, tag,
};
use crate::{ use crate::{
Parseable, Parseable,
ebuild::{Conditional, Depend, Eapi, Eclass, License, SrcUri, Uri, UriPrefix}, repo::ebuild::{Conditional, Depend, Eapi, Eclass, License, SrcUri, Uri, UriPrefix},
useflag::UseFlag, useflag::UseFlag,
}; };
@@ -22,7 +24,7 @@ impl<'a> Parseable<'a, &'a str> for Uri {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let protocol = alpha1::<&str>() let protocol = ascii_alpha1::<&str>()
.followed_by(tag("://")) .followed_by(tag("://"))
.map(|output: &str| output.to_string()); .map(|output: &str| output.to_string());
let path = r#if(|c: &char| !c.is_ascii_whitespace()) let path = r#if(|c: &char| !c.is_ascii_whitespace())
@@ -67,8 +69,11 @@ impl<'a> Parseable<'a, &'a str> for License {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric().or(one_of("_".chars())); let start = ascii_alphanumeric().or(one_of("_".chars()));
let rest = alphanumeric().or(one_of("+_.-".chars())).repeated().many(); let rest = ascii_alphanumeric()
.or(one_of("+_.-".chars()))
.repeated()
.many();
start start
.and(rest) .and(rest)
@@ -81,8 +86,11 @@ impl<'a> Parseable<'a, &'a str> for Eapi {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric().or(one_of("_".chars())); let start = ascii_alphanumeric().or(one_of("_".chars()));
let rest = alphanumeric().or(one_of("+_.-".chars())).repeated().many(); let rest = ascii_alphanumeric()
.or(one_of("+_.-".chars()))
.repeated()
.many();
start start
.and(rest) .and(rest)
@@ -116,23 +124,23 @@ where
|it| { |it| {
let exprs = || { let exprs = || {
Depend::parser() Depend::parser()
.separated_by_with_trailing(whitespace1()) .separated_by_with_trailing(ascii_whitespace1())
.at_least(1) .at_least(1)
.delimited_by(tag("(").followed_by(whitespace1()), tag(")")) .delimited_by(tag("(").followed_by(ascii_whitespace1()), tag(")"))
}; };
let all_of_group = exprs().map(|exprs| Depend::AllOf(exprs)); let all_of_group = exprs().map(|exprs| Depend::AllOf(exprs));
let any_of_group = exprs() let any_of_group = exprs()
.preceded_by(tag("||").followed_by(whitespace1())) .preceded_by(tag("||").followed_by(ascii_whitespace1()))
.map(|exprs| Depend::AnyOf(exprs)); .map(|exprs| Depend::AnyOf(exprs));
let one_of_group = exprs() let one_of_group = exprs()
.preceded_by(tag("^^").followed_by(whitespace1())) .preceded_by(tag("^^").followed_by(ascii_whitespace1()))
.map(|exprs| Depend::OneOf(exprs)); .map(|exprs| Depend::OneOf(exprs));
let conditional_group = Conditional::parser() let conditional_group = Conditional::parser()
.followed_by(whitespace1()) .followed_by(ascii_whitespace1())
.and(exprs()) .and(exprs())
.map(|(conditional, exprs)| Depend::ConditionalGroup(conditional, exprs)); .map(|(conditional, exprs)| Depend::ConditionalGroup(conditional, exprs));
@@ -166,7 +174,7 @@ mod test {
use mon::{ParserIter, input::InputIter}; use mon::{ParserIter, input::InputIter};
use crate::{atom::Atom, ebuild::Depend}; use crate::{atom::Atom, repo::ebuild::Depend};
use super::*; use super::*;
@@ -189,7 +197,7 @@ mod test {
let it = InputIter::new("flag? ( || ( foo/bar foo/bar ) )"); let it = InputIter::new("flag? ( || ( foo/bar foo/bar ) )");
Depend::<Atom>::parser() Depend::<Atom>::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.check_finished(it) .check_finished(it)
.unwrap(); .unwrap();

View File

@@ -5,15 +5,17 @@ use std::{
use get::Get; use get::Get;
use mon::{Parser, ParserIter, input::InputIter, tag, whitespace1}; use mon::{Parser, ParserIter, ascii_whitespace1, input::InputIter, tag};
use crate::{ use crate::{
Parseable, Parseable,
atom::{self, Atom}, atom::{self, Atom},
ebuild::{Depend, Eapi, Ebuild, Eclass, License, SrcUri}, repo::ebuild::{Depend, Eapi, Ebuild, Eclass, License, SrcUri},
useflag::IUseFlag, useflag::IUseFlag,
}; };
pub mod ebuild;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("io error: {0}")] #[error("io error: {0}")]
@@ -208,7 +210,7 @@ fn read_src_uri(input: &str) -> Option<Result<Vec<Depend<SrcUri>>, Error>> {
.find_map(|line| line.strip_prefix("SRC_URI="))?; .find_map(|line| line.strip_prefix("SRC_URI="))?;
match Depend::<SrcUri>::parser() match Depend::<SrcUri>::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.parse_finished(InputIter::new(line)) .parse_finished(InputIter::new(line))
{ {
@@ -232,7 +234,7 @@ fn read_inherit(input: &str) -> Option<Result<Vec<Eclass>, Error>> {
.find_map(|line| line.strip_prefix("INHERIT="))?; .find_map(|line| line.strip_prefix("INHERIT="))?;
match Eclass::parser() match Eclass::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.parse_finished(InputIter::new(line)) .parse_finished(InputIter::new(line))
{ {
@@ -245,7 +247,7 @@ fn read_iuse(input: &str) -> Option<Result<Vec<IUseFlag>, Error>> {
let line = input.lines().find_map(|line| line.strip_prefix("IUSE="))?; let line = input.lines().find_map(|line| line.strip_prefix("IUSE="))?;
match IUseFlag::parser() match IUseFlag::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.parse_finished(InputIter::new(line)) .parse_finished(InputIter::new(line))
{ {
@@ -260,7 +262,7 @@ fn read_license(input: &str) -> Option<Result<Vec<Depend<License>>, Error>> {
.find_map(|line| line.strip_suffix("LICENSE="))?; .find_map(|line| line.strip_suffix("LICENSE="))?;
match Depend::<License>::parser() match Depend::<License>::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.parse_finished(InputIter::new(line)) .parse_finished(InputIter::new(line))
{ {
@@ -309,7 +311,7 @@ fn read_idepend(input: &str) -> Option<Result<Vec<Depend<Atom>>, Error>> {
fn parse_depends(line: &str) -> Result<Vec<Depend<Atom>>, Error> { fn parse_depends(line: &str) -> Result<Vec<Depend<Atom>>, Error> {
Depend::<Atom>::parser() Depend::<Atom>::parser()
.separated_by(whitespace1()) .separated_by(ascii_whitespace1())
.many() .many()
.parse_finished(InputIter::new(line)) .parse_finished(InputIter::new(line))
.map_err(|_| Error::Parser(line.to_string())) .map_err(|_| Error::Parser(line.to_string()))

View File

@@ -2,12 +2,12 @@ use core::fmt;
use get::Get; use get::Get;
pub mod parsers; mod parsers;
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct UseFlag(#[get(method = "name", kind = "deref")] String); pub struct UseFlag(#[get(method = "name", kind = "deref")] String);
#[derive(Clone, Debug, PartialEq, Eq, Get)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
pub struct IUseFlag { pub struct IUseFlag {
default: bool, default: bool,
flag: UseFlag, flag: UseFlag,

View File

@@ -1,4 +1,4 @@
use mon::{Parser, ParserIter, alphanumeric, one_of, tag}; use mon::{Parser, ParserIter, ascii_alphanumeric, one_of, tag};
use crate::{ use crate::{
Parseable, Parseable,
@@ -9,8 +9,11 @@ impl<'a> Parseable<'a, &'a str> for UseFlag {
type Parser = impl Parser<&'a str, Output = Self>; type Parser = impl Parser<&'a str, Output = Self>;
fn parser() -> Self::Parser { fn parser() -> Self::Parser {
let start = alphanumeric(); let start = ascii_alphanumeric();
let rest = alphanumeric().or(one_of("+_@-".chars())).repeated().many(); let rest = ascii_alphanumeric()
.or(one_of("+_@-".chars()))
.repeated()
.many();
start start
.and(rest) .and(rest)

View File

@@ -1 +1,19 @@
subdir('fuzz') tests = {}
subdir('porthole')
subdir('repo')
foreach test, test_args : tests
stem = fs.stem(test)
test(
f'test_@stem@',
executable(
f'test_@stem@',
test,
dependencies: [mon, itertools],
link_with: [gentoo_utils],
),
args: test_args,
)
endforeach

View File

@@ -0,0 +1,5 @@
tests += {
meson.current_source_dir() / 'porthole.rs': [
meson.current_source_dir() / 'porthole.txt',
],
}

View File

@@ -1,4 +1,4 @@
use std::cmp::Ordering; use std::{cmp::Ordering, env, fs};
use gentoo_utils::{ use gentoo_utils::{
Parseable, Parseable,
@@ -6,11 +6,6 @@ use gentoo_utils::{
}; };
use mon::{Parser, input::InputIter, tag}; use mon::{Parser, input::InputIter, tag};
static PORTHOLE_TXT: &'static str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/testdata/porthole.txt"
));
enum Operator { enum Operator {
Comment, Comment,
Yes, Yes,
@@ -31,16 +26,21 @@ fn parse_operator<'a>() -> impl Parser<&'a str, Output = Operator> {
comment.or(yes).or(no).or(eq).or(gt).or(lt) comment.or(yes).or(no).or(eq).or(gt).or(lt)
} }
#[test] fn main() {
fn test_porthole() { let path = env::args()
for line in PORTHOLE_TXT.lines() { .nth(1)
.expect("pass path to porthole.txt as first parameter");
let porthole_txt = fs::read_to_string(&path).expect("failed to open porthole.txt");
for line in porthole_txt.lines() {
if line.is_empty() { if line.is_empty() {
continue; continue;
} }
let operator = parse_operator() let operator = parse_operator()
.parse_finished(InputIter::new( .parse_finished(InputIter::new(
line.split_ascii_whitespace().nth(0).unwrap(), line.split_ascii_whitespace().next().unwrap(),
)) ))
.unwrap(); .unwrap();

1
tests/repo/meson.build Normal file
View File

@@ -0,0 +1 @@
tests += {meson.current_source_dir() / 'repo.rs': []}

17
tests/repo/repo.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::error::Error;
use gentoo_utils::repo::Repo;
fn main() -> Result<(), Box<dyn Error>> {
let repo = Repo::new("/var/db/repos/gentoo");
for result in repo.categories()? {
let cat = result?;
for result in cat.ebuilds()? {
let _ = result?;
}
}
Ok(())
}