char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
a29d09f
main
1use std:: collections:: BTreeMap ; 2use std:: io:: Read ; 3use std:: path::{ Path , PathBuf }; 4 5use glob::{ MatchOptions , Pattern }; 6use sha2::{ Digest , Sha256 }; 7 8use crate ::{ Error , Result }; 9 10pub ( crate ) const MATCH_OPTIONS : MatchOptions =MatchOptions { 11case_sensitive : true , 12require_literal_separator : true , 13require_literal_leading_dot : false , 14}; 15 16# [ derive ( Debug , thiserror :: Error )] 17pub enum PatternError { 18# [ error ( "Glob patterns cannot be empty" )] 19Empty , 20# [ error ( "Glob patterns use '/' as the separator" )] 21Separator , 22# [ error ( transparent )] 23Syntax ( # [ from ] glob:: PatternError ), 24# [ error ( "Task outputs must be package-relative glob patterns that stay inside the package" )] 25OutputOutsidePackage , 26} 27 28pub fn validate_pattern ( pattern : & str ) -> std:: result:: Result <(), PatternError > { 29if pattern. is_empty () { 30return Err ( PatternError :: Empty ); 31} 32if pattern. contains ( '\\' ) { 33return Err ( PatternError :: Separator ); 34} 35Pattern :: new ( & normalize_pattern ( pattern)) ?; 36Ok (()) 37} 38 39pub fn validate_output_pattern ( pattern : & str ) -> std:: result:: Result <(), PatternError > { 40let normalized = pattern. strip_prefix ( "./" ). unwrap_or ( pattern); 41if normalized. is_empty () 42 || normalized =="." 43 || normalized. starts_with ( '/' ) 44 || normalized. split ( '/' ). any ( |part| part ==".." ) 45{ 46return Err ( PatternError :: OutputOutsidePackage ); 47} 48Ok (()) 49} 50 51/// Strips a leading `./` and trailing `/`s, which name the package root or 52/// a directory and carry no meaning for matching. 53pub fn normalize_pattern ( pattern : & str ) ->String { 54let mut normalized = pattern; 55while let Some ( rest) = normalized. strip_prefix ( "./" ) { 56 normalized = rest; 57} 58 normalized. trim_end_matches ( '/' ). to_owned () 59} 60 61/// Whether a pattern matches `relative` or a directory above it: a 62/// pattern naming a directory covers everything underneath it. 63pub fn covers ( pattern : & Pattern , relative : & str ) ->bool { 64if pattern. matches_with ( relative, MATCH_OPTIONS ) { 65return true ; 66} 67 relative 68. match_indices ( '/' ) 69. any ( |( index, _) | pattern. matches_with ( & relative[ ..index], MATCH_OPTIONS )) 70} 71 72pub fn compile_pattern ( task : & str , pattern : & str ) ->Result < Pattern > { 73Pattern :: new ( & normalize_pattern ( pattern)). map_err ( |error|Error :: Pattern { 74task : task. to_owned (), 75pattern : pattern. to_owned (), 76message : error. to_string (), 77}) 78} 79 80/// Expands `files` patterns against the tree under `root`. Every pattern 81/// must match at least one file. 82pub fn expand_files ( 83root : & Path , 84task : & str , 85patterns : & [ String ], 86) ->Result < BTreeMap < String , PathBuf >> { 87let files =collect_paths ( root, false ); 88let mut matched =BTreeMap :: new (); 89for patternin patterns{ 90let compiled =compile_pattern ( task, pattern) ?; 91let mut any =false ; 92for relativein & files{ 93if covers ( & compiled, relative) { 94 any =true ; 95 matched. insert ( relative. clone (), root. join ( relative)); 96} 97} 98if !any{ 99return Err ( Error :: UnmatchedFiles { 100task : task. to_owned (), 101pattern : pattern. to_owned (), 102}); 103} 104} 105Ok ( matched) 106} 107 108/// Slash-separated paths of everything under `base`, relative to it. 109pub fn collect_paths ( base : & Path , include_directories : bool ) ->Vec < String > { 110fn walk ( directory : & Path , prefix : & str , include_directories : bool , paths : & mut Vec < String >) { 111let Ok ( entries) = std:: fs:: read_dir ( directory) else { 112return ; 113}; 114let mut entries: Vec < _ > = entries. filter_map ( std:: result:: Result :: ok). collect (); 115 entries. sort_by_key ( |entry| entry. file_name ()); 116for entryin entries{ 117let name = entry. file_name (). to_string_lossy (). into_owned (); 118let relative =if prefix. is_empty () { 119 name 120} else { 121format! ( "{prefix}/{name}" ) 122}; 123let is_directory = entry. file_type (). is_ok_and ( |kind| kind. is_dir ()); 124if is_directory{ 125if include_directories{ 126 paths. push ( relative. clone ()); 127} 128walk ( & entry. path (), & relative, include_directories, paths); 129} else { 130 paths. push ( relative); 131} 132} 133} 134let mut paths =Vec :: new (); 135walk ( base, "" , include_directories, & mut paths); 136 paths 137} 138 139/// A file's cache identity: size and modification time, without reading it. 140pub ( crate ) fn file_id ( path : & Path ) -> std:: io:: Result < String > { 141let metadata = std:: fs:: metadata ( path) ?; 142let modified =match metadata. modified () { 143Ok ( mtime) => mtime. duration_since ( std:: time:: UNIX_EPOCH ). ok (), 144Err ( _) =>return hash_id ( path), 145}; 146let Some ( modified) = modifiedelse { 147return hash_id ( path); 148}; 149let mut id =format! ( 150"file:{}:{}:{}" , 151 metadata. len (), 152 modified. as_secs (), 153 modified. subsec_nanos () 154); 155if path 156. symlink_metadata () 157. is_ok_and ( |link| link. file_type (). is_symlink ()) 158 &&let Ok ( target) = std:: fs:: read_link ( path) 159{ 160 id. push_str ( & format! ( "->{}" , target. to_string_lossy ())); 161} 162Ok ( id) 163} 164 165fn hash_id ( path : & Path ) -> std:: io:: Result < String > { 166let mut file = std:: fs:: File :: open ( path) ?; 167let mut hasher =Sha256 :: new (); 168let mut buffer =[ 0 ; 8192 ]; 169loop { 170let read = file. read ( & mut buffer) ?; 171if read ==0 { 172break ; 173} 174 hasher. update ( & buffer[ ..read]); 175} 176Ok ( format! ( "file:hash:{:x}" , hasher. finalize ())) 177}