melib/utils: add fnmatch(3) interface

Meant for use with mailbox path globbing.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/492/head
Manos Pitsidianakis 4 weeks ago
parent 7f0157a966
commit be3b3ef89b
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

1
Cargo.lock generated

@ -1368,6 +1368,7 @@ dependencies = [
"async-io",
"base64 0.13.1",
"bitflags 2.6.0",
"cfg-if",
"chrono",
"data-encoding",
"encoding",

@ -22,6 +22,7 @@ async-fn-stream = { version = "=0.2.2" }
async-io = { version = "2" }
base64 = { version = "^0.13", optional = true }
bitflags = { version = "2.4", features = ["serde"] }
cfg-if = { version = "^1.0.0" }
chrono = { version = "^0.4", default-features = false }
data-encoding = { version = "2.1.1" }
encoding = { version = "0.2.33", default-features = false }

@ -0,0 +1,77 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! Interface for `libc`'s `fnmatch(3)`.
use std::ffi::CString;
mod ffi {
#![allow(dead_code)]
use std::os::raw::{c_char, c_int};
use cfg_if::cfg_if;
pub(super) const FNM_PERIOD: c_int = 1 << 2;
pub(super) const FNM_CASEFOLD: c_int = 1 << 4;
pub(super) const FNM_NOMATCH: c_int = 1;
cfg_if! {
if #[cfg(any(
target_os = "macos",
target_os = "freebsd",
target_os = "android",
))] {
pub(super) const FNM_PATHNAME: c_int = 1 << 1;
pub(super) const FNM_NOESCAPE: c_int = 1 << 0;
} else {
pub(super) const FNM_PATHNAME: c_int = 1 << 0;
pub(super) const FNM_NOESCAPE: c_int = 1 << 1;
}
}
extern "C" {
pub(super) fn fnmatch(pattern: *const c_char, name: *const c_char, flags: c_int) -> c_int;
}
}
/// See `fnmatch(3)`.
pub trait Fnmatch {
/// Returns `true` if glob pattern argument matches `self`.
fn fnmatches(&self, pattern: &str) -> bool;
}
impl Fnmatch for str {
fn fnmatches(&self, pattern: &str) -> bool {
let (Ok(name_c), Ok(pattern_c)) = (CString::new(self), CString::new(pattern)) else {
return false;
};
// SAFETY: both `pattern_c` and `name_c` are valid and NULL-terminated.
unsafe {
ffi::fnmatch(
pattern_c.as_c_str().as_ptr(),
name_c.as_c_str().as_ptr(),
ffi::FNM_PERIOD,
) == 0
}
}
}

@ -23,6 +23,7 @@
pub mod connections;
pub mod datetime;
pub mod fnmatch;
pub mod futures;
pub mod random;
pub mod vobject;

@ -334,3 +334,42 @@ fn test_fd_locks() {
)
.unwrap();
}
#[test]
fn test_fnmatch() {
use crate::utils::fnmatch::*;
for n in [
"INBOX",
"Folder",
"Something/with/separator",
"local.group.name",
] {
assert!(n.fnmatches("*"), "`*` should match with {:?}", n);
}
assert!(!".leading.period".fnmatches("*"));
assert!("INBOX/Sent".fnmatches("INBOX/*"));
assert!("INBOX/nested/mailbox".fnmatches("INBOX/*"));
assert!(!"INBOX".fnmatches("INBOX/*"));
assert!(!"Other".fnmatches("INBOX/*"));
assert!("nntp.taxonomy.niche".fnmatches("nntp.taxonomy.*"));
assert!(!"nntp.other".fnmatches("nntp.taxonomy.*"));
assert!("all.nested.niche".fnmatches("all.*"));
for n in ["Archives/2012", "Archives/2015"] {
assert!(
n.fnmatches("Archives/201?"),
"`Archives/201?` should match with {:?}",
n
);
}
for n in ["Archives/2005", "Archives/2021"] {
assert!(
!n.fnmatches("Archives/201?"),
"`Archives/201?` should not match with {:?}",
n
);
}
}

Loading…
Cancel
Save