pals 0.1.2

Processes' Arguments LiSt
Documentation
use std::{
    fs,
    path::Path,
};

use super::*;

#[derive( Debug, Deserialize )]
pub(crate) struct PsOutput {
    #[serde( rename = "process-information" )]
    pub(crate) proc_list : ProcList,
}

struct PidString;

impl<'de> de::Visitor<'de> for PidString {
    type Value = Pid;

    fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result {
        formatter.write_str( "an integer between 0 and 2^32" )
    }

    fn visit_str<E: de::Error>( self, value: &str ) -> Result<Self::Value, E> {
        u32::from_str_radix( value, 10 )
            .map( |v| Pid( v ))
            .map_err( |e| E::custom( e.to_string() ))
    }
}

pub(super) fn parse_id<'de, D>( deserializer: D ) -> Result<Pid, D::Error>
    where D: de::Deserializer<'de>
{
    Ok( deserializer.deserialize_str( PidString )? )
}

pub(super) fn pals_from_procfs( path: impl AsRef<Path> ) -> anyhow::Result<ProcList> {
    let mut procs = Vec::new();
    let dirs = fs::read_dir( path )?;
    for dir in dirs {
        if let Ok( entry ) = dir {
            let entry_path = entry.path();

            match fs::read( entry_path.join( "status" )) {
                Ok( status ) => {
                    let status = String::from_utf8( status )?;
                    let mut three_values = status.split_ascii_whitespace();
                    let command   = unescape( three_values.next().context( "missing command in status file" )? )?;
                    let pid       = Pid( u32::from_str_radix( three_values.next().context( "missing pid in status file" )?, 10 )? );
                    let ppid      = Pid( u32::from_str_radix( three_values.next().context( "missing ppid in status file" )?, 10 )? );
                    let arguments = str::from_utf8( &fs::read( entry_path.join( "cmdline" ))? )?.trim_end().to_owned();

                    procs.push( ProcNode {
                        process : Process{ pid, ppid, command, arguments },
                        link    : Link::default(),
                    });
                },
                Err( _ ) => continue,
            }
        }
    }

    if procs.is_empty() {
        return Err( anyhow!( "/proc is empty" ));
    }

    build_tree( ProcList{ procs, root: Link::default() }, ArgvStatus::Splitted )
}

fn unescape( input: &str ) -> anyhow::Result<String> {
    let mut output = String::with_capacity( input.len() );

    let mut chars = input.chars();
    let mut unescaping = Vec::<u8>::new();

    while let Some( ch ) = chars.next() {
        if ch == '\\' {
            output.push_str( &parse_octlets( &mut unescaping, &mut chars )? );
        } else {
            output.push( ch );
        }
    }

    Ok( output )
}

fn parse_octlets( unescaping: &mut Vec<u8>, chars: &mut impl Iterator<Item=char> ) -> anyhow::Result<String> {
    loop {
        let chars_next = chars.next();
        match chars_next {
            Some( '3' ) => {
                for _ in 0..7 {
                    match chars.next() {
                        Some('7') => (),
                        _ => return Err( anyhow!("bad escape of command name in status file: invalid unicode.")),
                    }
                }
                if let Some( ch ) = chars.next() {
                    let init = match ch {
                        '5' => 1 << 6,
                        '6' => 2 << 6,
                        '7' => 3 << 6,
                        _ => return Err( anyhow!("bad escape of command name in status file: invalid octlet {:?}.", ch )),
                    };
                    unescaping.push( u8_from_octlets( init, chars )? );
                    match str::from_utf8( unescaping ).map( ToOwned::to_owned ) {
                        Ok( s ) => {
                            unescaping.clear();
                            return Ok( s );
                        },
                        Err(_) => match chars.next() {
                            Some( '\\' ) => continue,
                            _ => return Err( anyhow!("bad escape of command name in status file: invalid utf8 {:?}.", unescaping )),
                        },
                    }
                } else {
                    return Err( anyhow!("bad escape of command name in status file: incomplete octlets."));
                }
            },
            None => return Err( anyhow!("bad escape of command name in status file: ends with backslash.")),
            Some( ch@_ ) => {
                let init = match ch {
                    '0' => 0,
                    '1' => 1 << 6,
                    _ => return Err( anyhow!("bad escape of command name in status file: invalid octlet {:?}.", ch )),
                };
                return Ok( str::from_utf8( &[ u8_from_octlets( init, chars )? ])?.to_owned() );
            },
        }
    }
}

fn u8_from_octlets( init: u8, chars: &mut impl Iterator<Item=char> ) -> anyhow::Result<u8> {
    let mut result = init;
    for i in 0..2 {
        match chars.next() {
            Some( ch@'0'..='7' ) => result += ( (ch as u8) - ('0' as u8) ) << (3*(1-i)),
            ch@_ => return Err( anyhow!("bad escape of command name in status file: invalid octlet {:?}.", ch)),
        }
    }
    Ok( result )
}