use super::*;
use crate::from_iter;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fs;
use std::fs::read_link;
use std::io::{self, Read};
#[cfg(target_os = "android")]
use std::os::android::fs::MetadataExt;
#[cfg(all(unix, not(target_os = "android")))]
use std::os::linux::fs::MetadataExt;
use std::path::PathBuf;
use std::str::FromStr;
mod limit;
pub use limit::*;
mod stat;
pub use stat::*;
mod mount;
pub use mount::*;
mod namespaces;
pub use namespaces::*;
mod status;
pub use status::*;
mod schedstat;
pub use schedstat::*;
mod task;
pub use task::*;
#[cfg(windows)]
trait FakeMedatadataExt {
fn st_uid(&self) -> u32;
}
#[cfg(windows)]
impl FakeMedatadataExt for std::fs::Metadata {
fn st_uid(&self) -> u32 {
panic!()
}
}
bitflags! {
pub struct StatFlags: u32 {
const PF_IDLE = 0x0000_0002;
const PF_EXITING = 0x0000_0004;
const PF_EXITPIDONE = 0x0000_0008;
const PF_VCPU = 0x0000_0010;
const PF_WQ_WORKER = 0x0000_0020;
const PF_FORKNOEXEC = 0x0000_0040;
const PF_MCE_PROCESS = 0x0000_0080;
const PF_SUPERPRIV = 0x0000_0100;
const PF_DUMPCORE = 0x0000_0200;
const PF_SIGNALED = 0x0000_0400;
const PF_MEMALLOC = 0x0000_0800;
const PF_NPROC_EXCEEDED = 0x0000_1000;
const PF_USED_MATH = 0x0000_2000;
const PF_USED_ASYNC = 0x0000_4000;
const PF_NOFREEZE = 0x0000_8000;
const PF_FROZEN = 0x0001_0000;
const PF_KSWAPD = 0x0002_0000;
const PF_MEMALLOC_NOFS = 0x0004_0000;
const PF_MEMALLOC_NOIO = 0x0008_0000;
const PF_LESS_THROTTLE = 0x0010_0000;
const PF_KTHREAD = 0x0020_0000;
const PF_RANDOMIZE = 0x0040_0000;
const PF_SWAPWRITE = 0x0080_0000;
const PF_MEMSTALL = 0x0100_0000;
const PF_UMH = 0x0200_0000;
const PF_NO_SETAFFINITY = 0x0400_0000;
const PF_MCE_EARLY = 0x0800_0000;
const PF_MEMALLOC_NOCMA = 0x1000_0000;
const PF_MUTEX_TESTER = 0x2000_0000;
const PF_FREEZER_SKIP = 0x4000_0000;
const PF_SUSPEND_TASK = 0x8000_0000;
}
}
bitflags! {
pub struct CoredumpFlags: u32 {
const ANONYMOUS_PRIVATE_MAPPINGS = 0x01;
const ANONYMOUS_SHARED_MAPPINGS = 0x02;
const FILEBACKED_PRIVATE_MAPPINGS = 0x04;
const FILEBACKED_SHARED_MAPPINGS = 0x08;
const ELF_HEADERS = 0x10;
const PROVATE_HUGEPAGES = 0x20;
const SHARED_HUGEPAGES = 0x40;
const PRIVATE_DAX_PAGES = 0x80;
const SHARED_DAX_PAGES = 0x100;
}
}
bitflags! {
pub struct FDPermissions: libc::mode_t {
const READ = libc::S_IRUSR;
const WRITE = libc::S_IWUSR;
const EXECUTE = libc::S_IXUSR;
}
}
bitflags! {
pub struct VmFlags: u32 {
const INVALID = 0;
const RD = 1 << 0;
const WR = 1 << 1;
const EX = 1 << 2;
const SH = 1 << 3;
const MR = 1 << 4;
const MW = 1 << 5;
const ME = 1 << 6;
const MS = 1 << 7;
const GD = 1 << 8;
const PF = 1 << 9;
const DW = 1 << 10;
const LO = 1 << 11;
const IO = 1 << 12;
const SR = 1 << 13;
const RR = 1 << 14;
const DC = 1 << 15;
const DE = 1 << 16;
const AC = 1 << 17;
const NR = 1 << 18;
const HT = 1 << 19;
const SF = 1 << 20;
const NL = 1 << 21;
const AR = 1 << 22;
const WF = 1 << 23;
const DD = 1 << 24;
const SD = 1 << 25;
const MM = 1 << 26;
const HG = 1 << 27;
const NH = 1 << 28;
const MG = 1 << 29;
const UM = 1 << 30;
const UW = 1 << 31;
}
}
impl VmFlags {
fn from_str(flag: &str) -> Option<Self> {
if flag.len() != 2 {
return None;
}
match flag {
"rd" => Some(VmFlags::RD),
"wr" => Some(VmFlags::WR),
"ex" => Some(VmFlags::EX),
"sh" => Some(VmFlags::SH),
"mr" => Some(VmFlags::MR),
"mw" => Some(VmFlags::MW),
"me" => Some(VmFlags::ME),
"ms" => Some(VmFlags::MS),
"gd" => Some(VmFlags::GD),
"pf" => Some(VmFlags::PF),
"dw" => Some(VmFlags::DW),
"lo" => Some(VmFlags::LO),
"io" => Some(VmFlags::IO),
"sr" => Some(VmFlags::SR),
"rr" => Some(VmFlags::RR),
"dc" => Some(VmFlags::DC),
"de" => Some(VmFlags::DE),
"ac" => Some(VmFlags::AC),
"nr" => Some(VmFlags::NR),
"ht" => Some(VmFlags::HT),
"sf" => Some(VmFlags::SF),
"nl" => Some(VmFlags::NL),
"ar" => Some(VmFlags::AR),
"wf" => Some(VmFlags::WF),
"dd" => Some(VmFlags::DD),
"sd" => Some(VmFlags::SD),
"mm" => Some(VmFlags::MM),
"hg" => Some(VmFlags::HG),
"nh" => Some(VmFlags::NH),
"mg" => Some(VmFlags::MG),
"um" => Some(VmFlags::UM),
"uw" => Some(VmFlags::UW),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ProcState {
Running,
Sleeping,
Waiting,
Zombie,
Stopped,
Tracing,
Dead,
Wakekill,
Waking,
Parked,
Idle,
}
impl ProcState {
pub fn from_char(c: char) -> Option<ProcState> {
match c {
'R' => Some(ProcState::Running),
'S' => Some(ProcState::Sleeping),
'D' => Some(ProcState::Waiting),
'Z' => Some(ProcState::Zombie),
'T' => Some(ProcState::Stopped),
't' => Some(ProcState::Tracing),
'X' | 'x' => Some(ProcState::Dead),
'K' => Some(ProcState::Wakekill),
'W' => Some(ProcState::Waking),
'P' => Some(ProcState::Parked),
'I' => Some(ProcState::Idle),
_ => None,
}
}
}
impl FromStr for ProcState {
type Err = ProcError;
fn from_str(s: &str) -> Result<ProcState, ProcError> {
ProcState::from_char(expect!(s.chars().next(), "empty string"))
.ok_or_else(|| build_internal_error!("failed to convert"))
}
}
#[derive(Debug, Copy, Clone)]
pub struct Io {
pub rchar: u64,
pub wchar: u64,
pub syscr: u64,
pub syscw: u64,
pub read_bytes: u64,
pub write_bytes: u64,
pub cancelled_write_bytes: u64,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum MMapPath {
Path(PathBuf),
Heap,
Stack,
TStack(u32),
Vdso,
Vvar,
Vsyscall,
Anonymous,
Vsys(i32),
Other(String),
}
impl MMapPath {
fn new() -> MMapPath {
MMapPath::Anonymous
}
fn from(path: &str) -> ProcResult<MMapPath> {
Ok(match path.trim() {
"" => MMapPath::Anonymous,
"[heap]" => MMapPath::Heap,
"[stack]" => MMapPath::Stack,
"[vdso]" => MMapPath::Vdso,
"[vvar]" => MMapPath::Vvar,
"[vsyscall]" => MMapPath::Vsyscall,
x if x.starts_with("[stack:") => {
let mut s = x[1..x.len() - 1].split(':');
let tid = from_str!(u32, expect!(s.nth(1)));
MMapPath::TStack(tid)
}
x if x.starts_with('[') && x.ends_with(']') => MMapPath::Other(x[1..x.len() - 1].to_string()),
x if x.starts_with("/SYSV") => MMapPath::Vsys(u32::from_str_radix(&x[5..13], 16)? as i32), x => MMapPath::Path(PathBuf::from(x)),
})
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct MemoryMap {
pub address: (u64, u64),
pub perms: String,
pub offset: u64,
pub dev: (i32, i32),
pub inode: u64,
pub pathname: MMapPath,
}
impl MemoryMap {
fn new() -> Self {
Self {
address: (0, 0),
perms: "".into(),
offset: 0,
dev: (0, 0),
inode: 0,
pathname: MMapPath::new(),
}
}
fn from_line(line: &str) -> ProcResult<MemoryMap> {
let mut s = line.splitn(6, ' ');
let address = expect!(s.next());
let perms = expect!(s.next());
let offset = expect!(s.next());
let dev = expect!(s.next());
let inode = expect!(s.next());
let path = expect!(s.next());
Ok(MemoryMap {
address: split_into_num(address, '-', 16)?,
perms: perms.to_string(),
offset: from_str!(u64, offset, 16),
dev: split_into_num(dev, ':', 16)?,
inode: from_str!(u64, inode),
pathname: MMapPath::from(path)?,
})
}
}
#[derive(Default, Debug)]
pub struct MemoryMapData {
pub map: HashMap<String, u64>,
pub vm_flags: Option<VmFlags>,
}
impl Io {
pub fn from_reader<R: io::Read>(r: R) -> ProcResult<Io> {
let mut map = HashMap::new();
let reader = BufReader::new(r);
for line in reader.lines() {
let line = line?;
if line.is_empty() || !line.contains(' ') {
continue;
}
let mut s = line.split_whitespace();
let field = expect!(s.next());
let value = expect!(s.next());
let value = from_str!(u64, value);
map.insert(field[..field.len() - 1].to_string(), value);
}
let io = Io {
rchar: expect!(map.remove("rchar")),
wchar: expect!(map.remove("wchar")),
syscr: expect!(map.remove("syscr")),
syscw: expect!(map.remove("syscw")),
read_bytes: expect!(map.remove("read_bytes")),
write_bytes: expect!(map.remove("write_bytes")),
cancelled_write_bytes: expect!(map.remove("cancelled_write_bytes")),
};
assert!(!(cfg!(test) && !map.is_empty()), "io map is not empty: {:#?}", map);
Ok(io)
}
}
#[derive(Clone, Debug)]
pub enum FDTarget {
Path(PathBuf),
Socket(u64),
Net(u64),
Pipe(u64),
AnonInode(String),
MemFD(String),
Other(String, u64),
}
impl FromStr for FDTarget {
type Err = ProcError;
fn from_str(s: &str) -> Result<FDTarget, ProcError> {
fn strip_first_last(s: &str) -> ProcResult<&str> {
if s.len() > 2 {
let mut c = s.chars();
let _ = c.next();
let _ = c.next_back();
Ok(c.as_str())
} else {
Err(ProcError::Incomplete(None))
}
}
if !s.starts_with('/') && s.contains(':') {
let mut s = s.split(':');
let fd_type = expect!(s.next());
match fd_type {
"socket" => {
let inode = expect!(s.next(), "socket inode");
let inode = expect!(u64::from_str_radix(strip_first_last(inode)?, 10));
Ok(FDTarget::Socket(inode))
}
"net" => {
let inode = expect!(s.next(), "net inode");
let inode = expect!(u64::from_str_radix(strip_first_last(inode)?, 10));
Ok(FDTarget::Net(inode))
}
"pipe" => {
let inode = expect!(s.next(), "pipe inode");
let inode = expect!(u64::from_str_radix(strip_first_last(inode)?, 10));
Ok(FDTarget::Pipe(inode))
}
"anon_inode" => Ok(FDTarget::AnonInode(expect!(s.next(), "anon inode").to_string())),
"/memfd" => Ok(FDTarget::MemFD(expect!(s.next(), "memfd name").to_string())),
"" => Err(ProcError::Incomplete(None)),
x => {
let inode = expect!(s.next(), "other inode");
let inode = expect!(u64::from_str_radix(strip_first_last(inode)?, 10));
Ok(FDTarget::Other(x.to_string(), inode))
}
}
} else {
Ok(FDTarget::Path(PathBuf::from(s)))
}
}
}
#[derive(Clone)]
pub struct FDInfo {
pub fd: u32,
pub mode: libc::mode_t,
pub target: FDTarget,
}
impl FDInfo {
pub fn from_raw_fd(pid: pid_t, raw_fd: i32) -> ProcResult<Self> {
Self::from_raw_fd_with_root("/proc", pid, raw_fd)
}
pub fn from_raw_fd_with_root(root: impl AsRef<Path>, pid: pid_t, raw_fd: i32) -> ProcResult<Self> {
let path = root.as_ref().join(pid.to_string()).join("fd").join(raw_fd.to_string());
let link = wrap_io_error!(path, read_link(&path))?;
let md = wrap_io_error!(path, path.symlink_metadata())?;
let link_os: &OsStr = link.as_ref();
Ok(Self {
fd: raw_fd as u32,
mode: (md.st_mode() as libc::mode_t) & libc::S_IRWXU,
target: expect!(FDTarget::from_str(expect!(link_os.to_str()))),
})
}
pub fn mode(&self) -> FDPermissions {
FDPermissions::from_bits_truncate(self.mode)
}
}
impl std::fmt::Debug for FDInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FDInfo {{ fd: {:?}, mode: 0{:o}, target: {:?} }}",
self.fd, self.mode, self.target
)
}
}
#[derive(Debug, Clone)]
pub struct Process {
pub pid: i32,
pub stat: Stat,
pub owner: u32,
pub(crate) root: PathBuf,
}
impl Process {
pub fn new(pid: pid_t) -> ProcResult<Process> {
let root = PathBuf::from("/proc").join(format!("{}", pid));
Self::new_with_root(root)
}
pub fn new_with_root(root: PathBuf) -> ProcResult<Process> {
let path = root.join("stat");
let stat = Stat::from_reader(FileWrapper::open(&path)?)?;
let md = std::fs::metadata(&root)?;
Ok(Process {
pid: stat.pid,
root,
stat,
owner: md.st_uid(),
})
}
pub fn myself() -> ProcResult<Process> {
let root = PathBuf::from("/proc/self");
Self::new_with_root(root)
}
pub fn cmdline(&self) -> ProcResult<Vec<String>> {
let mut buf = String::new();
let mut f = FileWrapper::open(self.root.join("cmdline"))?;
f.read_to_string(&mut buf)?;
Ok(buf
.split('\0')
.filter_map(|s| if !s.is_empty() { Some(s.to_string()) } else { None })
.collect())
}
pub fn pid(&self) -> pid_t {
self.stat.pid
}
pub fn is_alive(&self) -> bool {
match Process::new(self.pid()) {
Ok(prc) => {
prc.stat.comm == self.stat.comm
&& prc.owner == self.owner
&& prc.stat.starttime == self.stat.starttime
&& prc.stat.state().map(|s| s != ProcState::Zombie).unwrap_or(false)
&& self.stat.state().map(|s| s != ProcState::Zombie).unwrap_or(false)
}
_ => false,
}
}
pub fn cwd(&self) -> ProcResult<PathBuf> {
Ok(std::fs::read_link(self.root.join("cwd"))?)
}
pub fn root(&self) -> ProcResult<PathBuf> {
Ok(std::fs::read_link(self.root.join("root"))?)
}
pub fn environ(&self) -> ProcResult<HashMap<OsString, OsString>> {
use std::os::unix::ffi::OsStrExt;
let mut map = HashMap::new();
let mut file = FileWrapper::open(self.root.join("environ"))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
for slice in buf.split(|b| *b == 0) {
let mut split = slice.splitn(2, |b| *b == b'=');
if let (Some(k), Some(v)) = (split.next(), split.next()) {
map.insert(OsStr::from_bytes(k).to_os_string(), OsStr::from_bytes(v).to_os_string());
};
}
Ok(map)
}
pub fn exe(&self) -> ProcResult<PathBuf> {
Ok(std::fs::read_link(self.root.join("exe"))?)
}
pub fn io(&self) -> ProcResult<Io> {
let path = self.root.join("io");
let file = FileWrapper::open(&path)?;
Io::from_reader(file)
}
pub fn maps(&self) -> ProcResult<Vec<MemoryMap>> {
let path = self.root.join("maps");
let file = FileWrapper::open(&path)?;
let reader = BufReader::new(file);
let mut vec = Vec::new();
for line in reader.lines() {
let line = line.map_err(|_| ProcError::Incomplete(Some(path.clone())))?;
vec.push(MemoryMap::from_line(&line)?);
}
Ok(vec)
}
pub fn smaps(&self) -> ProcResult<Vec<(MemoryMap, MemoryMapData)>> {
let path = self.root.join("smaps");
let file = FileWrapper::open(&path)?;
let reader = BufReader::new(file);
let mut vec: Vec<(MemoryMap, MemoryMapData)> = Vec::new();
let mut current_mapping = MemoryMap::new();
let mut current_data = Default::default();
for line in reader.lines() {
let line = line.map_err(|_| ProcError::Incomplete(Some(path.clone())))?;
if let Ok(mapping) = MemoryMap::from_line(&line) {
vec.push((current_mapping, current_data));
current_mapping = mapping;
current_data = Default::default();
} else {
if line.starts_with("VmFlags") {
let flags = line.split_ascii_whitespace();
let flags = flags.skip(1);
let flags = flags
.map(|v| match VmFlags::from_str(v) {
None => VmFlags::INVALID,
Some(v) => v,
})
.fold(VmFlags::INVALID, |a, b| a | b);
current_data.vm_flags = Some(flags);
} else {
let mut parts = line.split_ascii_whitespace();
let key = parts.next();
let value = parts.next();
if let (Some(k), Some(v)) = (key, value) {
let size_suffix = parts.next();
let size_multiplier = if size_suffix.is_some() { 1024 } else { 1 };
let v = v.parse::<u64>().map_err(|_| {
ProcError::Other("Value in `Key: Value` pair was not actually a number".into())
})?;
let k = k.trim_end_matches(':');
current_data.map.insert(k.into(), v * size_multiplier);
}
}
}
}
Ok(vec)
}
pub fn fd_count(&self) -> ProcResult<usize> {
let path = self.root.join("fd");
Ok(wrap_io_error!(path, path.read_dir())?.count())
}
pub fn fd(&self) -> ProcResult<Vec<FDInfo>> {
let mut vec = Vec::new();
let path = self.root.join("fd");
for dir in wrap_io_error!(path, path.read_dir())? {
let entry = dir?;
let file_name = entry.file_name();
let fd = from_str!(u32, expect!(file_name.to_str()), 10);
if let (Ok(link), Ok(md)) = (read_link(entry.path()), entry.metadata()) {
let link_os: &OsStr = link.as_ref();
vec.push(FDInfo {
fd,
mode: (md.st_mode() as libc::mode_t) & libc::S_IRWXU,
target: expect!(FDTarget::from_str(expect!(link_os.to_str()))),
});
}
}
Ok(vec)
}
pub fn coredump_filter(&self) -> ProcResult<Option<CoredumpFlags>> {
let mut file = FileWrapper::open(self.root.join("coredump_filter"))?;
let mut s = String::new();
file.read_to_string(&mut s)?;
if s.trim().is_empty() {
return Ok(None);
}
let flags = from_str!(u32, &s.trim(), 16, pid:self.stat.pid);
Ok(Some(expect!(CoredumpFlags::from_bits(flags))))
}
pub fn autogroup(&self) -> ProcResult<String> {
let mut s = String::new();
let mut file = FileWrapper::open(self.root.join("autogroup"))?;
file.read_to_string(&mut s)?;
Ok(s)
}
pub fn auxv(&self) -> ProcResult<HashMap<u32, u32>> {
use byteorder::{NativeEndian, ReadBytesExt};
let mut file = FileWrapper::open(self.root.join("auxv"))?;
let mut map = HashMap::new();
let mut buf = Vec::new();
let bytes_read = file.read_to_end(&mut buf)?;
if bytes_read == 0 {
return Ok(map);
}
buf.truncate(bytes_read);
let mut file = std::io::Cursor::new(buf);
loop {
let key = file.read_u32::<NativeEndian>()?;
let value = file.read_u32::<NativeEndian>()?;
if key == 0 && value == 0 {
break;
}
map.insert(key, value);
}
Ok(map)
}
pub fn wchan(&self) -> ProcResult<String> {
let mut s = String::new();
let mut file = FileWrapper::open(self.root.join("wchan"))?;
file.read_to_string(&mut s)?;
Ok(s)
}
pub fn status(&self) -> ProcResult<Status> {
let path = self.root.join("status");
let file = FileWrapper::open(&path)?;
Status::from_reader(file)
}
pub fn stat(&self) -> ProcResult<Stat> {
let path = self.root.join("stat");
let stat = Stat::from_reader(FileWrapper::open(&path)?)?;
Ok(stat)
}
pub fn loginuid(&self) -> ProcResult<u32> {
let mut uid = String::new();
let path = self.root.join("loginuid");
let mut file = FileWrapper::open(&path)?;
file.read_to_string(&mut uid)?;
Status::parse_uid_gid(&uid, 0)
}
pub fn oom_score(&self) -> ProcResult<u32> {
let path = self.root.join("oom_score");
let mut file = FileWrapper::open(&path)?;
let mut oom = String::new();
file.read_to_string(&mut oom)?;
Ok(from_str!(u32, oom.trim()))
}
pub fn statm(&self) -> ProcResult<StatM> {
let path = self.root.join("statm");
let file = FileWrapper::open(&path)?;
StatM::from_reader(file)
}
pub fn task_main_thread(&self) -> ProcResult<Task> {
Task::new(self.pid, self.pid)
}
pub fn schedstat(&self) -> ProcResult<Schedstat> {
let path = self.root.join("schedstat");
let file = FileWrapper::open(&path)?;
Schedstat::from_reader(file)
}
pub fn tasks(&self) -> ProcResult<TasksIter> {
Ok(TasksIter {
pid: self.pid,
inner: fs::read_dir(self.root.join("task"))?,
})
}
}
#[derive(Debug)]
pub struct TasksIter {
pid: pid_t,
inner: fs::ReadDir,
}
impl std::iter::Iterator for TasksIter {
type Item = ProcResult<Task>;
fn next(&mut self) -> Option<ProcResult<Task>> {
match self.inner.next() {
Some(Ok(tp)) => Some(Task::from_rel_path(self.pid, &tp.path())),
Some(Err(e)) => Some(Err(ProcError::Io(e, None))),
None => None,
}
}
}
pub fn all_processes() -> ProcResult<Vec<Process>> {
all_processes_with_root("/proc")
}
pub fn all_processes_with_root(root: impl AsRef<Path>) -> ProcResult<Vec<Process>> {
let mut v = Vec::new();
let root = root.as_ref();
for entry in expect!(std::fs::read_dir(root), format!("No {} directory", root.display())).flatten() {
if i32::from_str(&entry.file_name().to_string_lossy()).is_ok() {
match Process::new_with_root(entry.path()) {
Ok(prc) => v.push(prc),
Err(ProcError::InternalError(e)) => return Err(ProcError::InternalError(e)),
_ => {}
}
}
}
Ok(v)
}
#[derive(Debug, Clone, Copy)]
pub struct StatM {
pub size: u64,
pub resident: u64,
pub shared: u64,
pub text: u64,
pub lib: u64,
pub data: u64,
pub dt: u64,
}
impl StatM {
fn from_reader<R: io::Read>(mut r: R) -> ProcResult<StatM> {
let mut line = String::new();
r.read_to_string(&mut line)?;
let mut s = line.split_whitespace();
let size = expect!(from_iter(&mut s));
let resident = expect!(from_iter(&mut s));
let shared = expect!(from_iter(&mut s));
let text = expect!(from_iter(&mut s));
let lib = expect!(from_iter(&mut s));
let data = expect!(from_iter(&mut s));
let dt = expect!(from_iter(&mut s));
if cfg!(test) {
assert!(s.next().is_none());
}
Ok(StatM {
size,
resident,
shared,
text,
lib,
data,
dt,
})
}
}
#[cfg(test)]
mod tests;