11//! Python bindings for the boreal library.
2+ #![ allow( unsafe_code) ]
3+
4+ use std:: fs:: File ;
5+ use std:: io:: Read ;
6+ use std:: os:: fd:: { FromRawFd , OwnedFd } ;
7+
8+ use pyo3:: create_exception;
9+ use pyo3:: exceptions:: { PyException , PyTypeError } ;
10+ use pyo3:: ffi;
211use pyo3:: prelude:: * ;
12+ use pyo3:: types:: PyDict ;
313
4- use :: boreal:: Compiler ;
14+ use :: boreal:: compiler ;
515
616// TODO: all clone impls should be efficient...
717// TODO: should all pyclasses have names and be exposed in the module?
@@ -11,23 +21,164 @@ mod scanner;
1121mod string_match_instance;
1222mod string_matches;
1323
24+ create_exception ! ( boreal, AddRuleError , PyException , "error when adding rules" ) ;
25+
1426#[ pymodule]
1527fn boreal ( m : & Bound < ' _ , PyModule > ) -> PyResult < ( ) > {
16- m. add_function ( wrap_pyfunction ! ( compile, m) ?)
28+ m. add_function ( wrap_pyfunction ! ( compile, m) ?) ?;
29+ m. add ( "AddRuleError" , m. py ( ) . get_type :: < AddRuleError > ( ) ) ?;
30+ Ok ( ( ) )
1731}
1832
33+ // TODO: add strict_escape?
1934#[ pyfunction]
20- #[ pyo3( signature = ( filepath=None , source=None ) ) ]
21- fn compile ( filepath : Option < & str > , source : Option < & str > ) -> PyResult < scanner:: PyScanner > {
22- let mut compiler = Compiler :: new ( ) ;
23- match ( filepath, source) {
24- ( Some ( v) , None ) => compiler. add_rules_file ( v) ,
25- ( None , Some ( v) ) => compiler. add_rules_str ( v) ,
26- _ => todo ! ( ) ,
35+ #[ pyo3( signature = ( filepath=None , source=None , file=None , filepaths=None , sources=None , externals=None , includes=true , error_on_warning=false ) ) ]
36+ #[ allow( clippy:: too_many_arguments) ]
37+ fn compile (
38+ filepath : Option < & str > ,
39+ source : Option < & str > ,
40+ file : Option < & Bound < ' _ , PyAny > > ,
41+ filepaths : Option < & Bound < ' _ , PyDict > > ,
42+ sources : Option < & Bound < ' _ , PyDict > > ,
43+ externals : Option < & Bound < ' _ , PyDict > > ,
44+ includes : bool ,
45+ error_on_warning : bool ,
46+ ) -> PyResult < scanner:: PyScanner > {
47+ let mut compiler = compiler:: Compiler :: new ( ) ;
48+ compiler. set_params (
49+ compiler:: CompilerParams :: default ( )
50+ . disable_includes ( !includes)
51+ . fail_on_warnings ( error_on_warning) ,
52+ ) ;
53+ if let Some ( externals) = externals {
54+ add_externals ( & mut compiler, externals) ?;
55+ }
56+
57+ let mut warnings = Vec :: new ( ) ;
58+
59+ match ( filepath, source, file, filepaths, sources) {
60+ ( Some ( filepath) , None , None , None , None ) => {
61+ let res = compiler
62+ . add_rules_file ( filepath)
63+ // TODO: contents
64+ . map_err ( |err| convert_compiler_error ( & err, filepath, "" ) )
65+ . map_err ( AddRuleError :: new_err) ?;
66+ warnings = res
67+ . warnings ( )
68+ . map ( |err| convert_compiler_error ( err, filepath, "" ) )
69+ . collect ( ) ;
70+ }
71+ ( None , Some ( source) , None , None , None ) => {
72+ let res = compiler
73+ . add_rules_str ( source)
74+ . map_err ( |err| convert_compiler_error ( & err, "source" , source) )
75+ . map_err ( AddRuleError :: new_err) ?;
76+ warnings = res
77+ . warnings ( )
78+ . map ( |err| convert_compiler_error ( err, "source" , source) )
79+ . collect ( ) ;
80+ }
81+ ( None , None , Some ( file) , None , None ) => {
82+ // Safety:
83+ // - `as_ptr` is safe because it does not outlive file
84+ // - `PyObject_AsFileDescriptor` is safe to call on any valid PyObject.
85+ // if it fails, -1 is returned.
86+ let fd = unsafe { ffi:: PyObject_AsFileDescriptor ( file. as_ptr ( ) ) } ;
87+ if fd == -1 {
88+ return Err ( PyTypeError :: new_err ( "`file` argument is not a file object" ) ) ;
89+ }
90+
91+ // Safety: the passed file descriptor is valid and thus can be dupped.
92+ let owned_fd = unsafe { libc:: dup ( fd) } ;
93+ if owned_fd == -1 {
94+ return Err ( std:: io:: Error :: last_os_error ( ) . into ( ) ) ;
95+ }
96+
97+ // Safety: the file descriptor is valid and is owned by us.
98+ let owned_fd = unsafe { OwnedFd :: from_raw_fd ( owned_fd) } ;
99+
100+ let mut file = File :: from ( owned_fd) ;
101+ let mut contents = String :: new ( ) ;
102+ let _r = file. read_to_string ( & mut contents) ?;
103+ // TODO: this makes the error message not as nice
104+ let res = compiler
105+ . add_rules_str ( & contents)
106+ . map_err ( |err| convert_compiler_error ( & err, "file" , & contents) )
107+ . map_err ( AddRuleError :: new_err) ?;
108+ warnings = res
109+ . warnings ( )
110+ . map ( |err| convert_compiler_error ( err, "file" , & contents) )
111+ . collect ( ) ;
112+ }
113+ ( None , None , None , Some ( filepaths) , None ) => {
114+ for ( key, value) in filepaths {
115+ let namespace: & str = key. extract ( ) . map_err ( |_| {
116+ PyTypeError :: new_err ( "keys of the `filepaths` argument must be strings" )
117+ } ) ?;
118+ let filepath: & str = value. extract ( ) . map_err ( |_| {
119+ PyTypeError :: new_err ( "values of the `filepaths` argument must be strings" )
120+ } ) ?;
121+ let res = compiler
122+ . add_rules_file_in_namespace ( filepath, namespace)
123+ // TODO: contents
124+ . map_err ( |err| convert_compiler_error ( & err, filepath, "" ) )
125+ . map_err ( AddRuleError :: new_err) ?;
126+ warnings. extend (
127+ res. warnings ( )
128+ . map ( |err| convert_compiler_error ( err, filepath, "" ) ) ,
129+ ) ;
130+ }
131+ }
132+ ( None , None , None , None , Some ( sources) ) => {
133+ for ( key, value) in sources {
134+ let namespace: & str = key. extract ( ) . map_err ( |_| {
135+ PyTypeError :: new_err ( "keys of the `sources` argument must be strings" )
136+ } ) ?;
137+ let source: & str = value. extract ( ) . map_err ( |_| {
138+ PyTypeError :: new_err ( "values of the `sources` argument must be strings" )
139+ } ) ?;
140+ let res = compiler
141+ . add_rules_str_in_namespace ( source, namespace)
142+ . map_err ( |err| convert_compiler_error ( & err, namespace, source) )
143+ . map_err ( AddRuleError :: new_err) ?;
144+ warnings. extend (
145+ res. warnings ( )
146+ . map ( |err| convert_compiler_error ( err, namespace, source) ) ,
147+ ) ;
148+ }
149+ }
150+ _ => return Err ( PyTypeError :: new_err ( "invalid arguments passed" ) ) ,
27151 }
28- . unwrap ( ) ;
29152
30153 Ok ( scanner:: PyScanner {
31154 scanner : compiler. into_scanner ( ) ,
155+ warnings,
32156 } )
33157}
158+
159+ fn convert_compiler_error ( err : & compiler:: AddRuleError , input_name : & str , input : & str ) -> String {
160+ err. to_short_description ( input_name, input)
161+ }
162+
163+ fn add_externals ( compiler : & mut compiler:: Compiler , externals : & Bound < ' _ , PyDict > ) -> PyResult < ( ) > {
164+ for ( key, value) in externals {
165+ let name: & str = key. extract ( ) ?;
166+
167+ if let Ok ( v) = value. extract :: < bool > ( ) {
168+ let _r = compiler. define_symbol ( name, v) ;
169+ } else if let Ok ( v) = value. extract :: < i64 > ( ) {
170+ let _r = compiler. define_symbol ( name, v) ;
171+ } else if let Ok ( v) = value. extract :: < f64 > ( ) {
172+ let _r = compiler. define_symbol ( name, v) ;
173+ } else if let Ok ( v) = value. extract :: < & str > ( ) {
174+ let _r = compiler. define_symbol ( name, v) ;
175+ } else if let Ok ( v) = value. extract :: < & [ u8 ] > ( ) {
176+ let _r = compiler. define_symbol ( name, v) ;
177+ } else {
178+ return Err ( PyTypeError :: new_err (
179+ "invalid type for the external value, must be a boolean, integer, float or string" ,
180+ ) ) ;
181+ }
182+ }
183+ Ok ( ( ) )
184+ }
0 commit comments