lib.rs (24276B)
1 //! Python wrapper for Brave's adblocking library, which is written in Rust. 2 #![deny( 3 future_incompatible, 4 nonstandard_style, 5 rust_2018_idioms, 6 missing_copy_implementations, 7 trivial_casts, 8 trivial_numeric_casts, 9 unsafe_code, 10 unused_qualifications, 11 deprecated 12 )] 13 14 use adblock::blocker::BlockerResult as RustBlockerResult; 15 use adblock::cosmetic_filter_cache::UrlSpecificResources as RustUrlSpecificResources; 16 use adblock::engine::Engine as RustEngine; 17 use adblock::lists::FilterSet as RustFilterSet; 18 use adblock::lists::{FilterFormat, ParseOptions, RuleTypes}; 19 use adblock::request::Request; 20 use pyo3::create_exception; 21 use pyo3::exceptions::PyException; 22 use pyo3::prelude::*; 23 use pyo3::types::PyBytes; 24 use pyo3::PyErr; 25 26 use adblock::resources::{MimeType, PermissionMask, Resource, ResourceType}; 27 use std::collections::HashSet; 28 use std::error::Error; 29 use std::fmt::{self, Display}; 30 use std::fs; 31 use std::io::{Read, Write}; 32 33 /// Brave's adblocking library in Python! 34 #[pymodule] 35 fn adblock_py(m: &Bound<'_, PyModule>) -> PyResult<()> { 36 m.add("__version__", env!("CARGO_PKG_VERSION"))?; 37 m.add_class::<Engine>()?; 38 m.add_class::<FilterSet>()?; 39 m.add_class::<BlockerResult>()?; 40 m.add_class::<UrlSpecificResources>()?; 41 m.add("AdblockException", m.py().get_type_bound::<AdblockException>())?; 42 m.add("BlockerException", m.py().get_type_bound::<BlockerException>())?; 43 m.add("SerializationError", m.py().get_type_bound::<SerializationError>())?; 44 m.add( 45 "DeserializationError", 46 m.py().get_type_bound::<DeserializationError>(), 47 )?; 48 m.add( 49 "OptimizedFilterExistence", 50 m.py().get_type_bound::<OptimizedFilterExistence>(), 51 )?; 52 m.add( 53 "BadFilterAddUnsupported", 54 m.py().get_type_bound::<BadFilterAddUnsupported>(), 55 )?; 56 m.add("FilterExists", m.py().get_type_bound::<FilterExists>())?; 57 m.add( 58 "AddResourceException", 59 m.py().get_type_bound::<AddResourceException>(), 60 )?; 61 m.add( 62 "InvalidBase64ContentError", 63 m.py().get_type_bound::<InvalidBase64ContentError>(), 64 )?; 65 m.add( 66 "InvalidUtf8ContentError", 67 m.py().get_type_bound::<InvalidUtf8ContentError>(), 68 )?; 69 Ok(()) 70 } 71 72 /// The result of an ad-blocking check. 73 #[pyclass] 74 pub struct BlockerResult { 75 #[pyo3(get)] 76 pub matched: bool, 77 /// Important is used to signal that a rule with the `important` option 78 /// matched. An `important` match means that exceptions should not apply 79 /// and no further checking is neccesary--the request should be blocked 80 /// (empty body or cancelled). 81 /// 82 /// Brave Browser keeps seperate instances of Blocker for default lists 83 /// and regional ones, so `important` here is used to correct behaviour 84 /// between them: checking should stop instead of moving to the next 85 /// instance iff an `important` rule matched. 86 #[pyo3(get)] 87 pub important: bool, 88 /// Iff the blocker matches a rule which has the `redirect` option, as per 89 /// [uBlock Origin's redirect syntax][1], the `redirect` is not `None`. 90 /// The `redirect` field contains the body of the redirect to be injected. 91 /// 92 /// [1]: https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#redirect 93 #[pyo3(get)] 94 pub redirect: Option<String>, 95 /// `removeparam` may remove URL parameters. If the original request URL was 96 /// modified at all, the new version will be here. 97 #[pyo3(get)] 98 pub rewritten_url: Option<String>, 99 /// Exception is not `None` when the blocker matched on an exception rule. 100 /// Effectively this means that there was a match, but the request should 101 /// not be blocked. It is a non-empty string if the blocker was initialized 102 /// from a list of rules with debugging enabled, otherwise the original 103 /// string representation is discarded to reduce memory use. 104 #[pyo3(get)] 105 pub exception: Option<String>, 106 /// Filter--similarly to exception--includes the string representation of 107 /// the rule when there is a match and debugging is enabled. Otherwise, on 108 /// a match, it is not `None`. 109 #[pyo3(get)] 110 pub filter: Option<String>, 111 } 112 113 impl From<RustBlockerResult> for BlockerResult { 114 fn from(br: RustBlockerResult) -> Self { 115 Self { 116 matched: br.matched, 117 important: br.important, 118 redirect: br.redirect, 119 rewritten_url: br.rewritten_url, 120 exception: br.exception, 121 filter: br.filter, 122 } 123 } 124 } 125 126 #[pymethods] 127 impl BlockerResult { 128 fn __repr__(&self) -> PyResult<String> { 129 Ok(format!( 130 "BlockerResult(matched={}, important={}, redirect={}, rewritten_url={}, exception={}, filter={})", 131 self.matched.diy_python_repr(), 132 self.important.diy_python_repr(), 133 self.redirect.diy_python_repr(), 134 self.rewritten_url.diy_python_repr(), 135 self.exception.diy_python_repr(), 136 self.filter.diy_python_repr(), 137 )) 138 } 139 } 140 141 #[derive(Debug, PartialEq, Eq, Copy, Clone)] 142 pub enum BlockerError { 143 SerializationError, 144 DeserializationError, 145 OptimizedFilterExistence, 146 BadFilterAddUnsupported, 147 FilterExists, 148 } 149 150 impl Error for BlockerError { 151 fn source(&self) -> Option<&(dyn Error + 'static)> { 152 None 153 } 154 } 155 156 impl Display for BlockerError { 157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 158 write!( 159 f, 160 "{}", 161 match self { 162 Self::SerializationError => "Serialization error", 163 Self::DeserializationError => "Deserialization error", 164 Self::OptimizedFilterExistence => "Optimized filter exists", 165 Self::BadFilterAddUnsupported => "Bad filter add unsupported", 166 Self::FilterExists => "Filter exists", 167 } 168 ) 169 } 170 } 171 172 create_exception!(adblock, AdblockException, PyException); 173 create_exception!(adblock, BlockerException, AdblockException); 174 create_exception!(adblock, AddResourceException, AdblockException); 175 create_exception!(adblock, InvalidBase64ContentError, AddResourceException); 176 create_exception!(adblock, InvalidUtf8ContentError, AddResourceException); 177 create_exception!(adblock, SerializationError, BlockerException); 178 create_exception!(adblock, DeserializationError, BlockerException); 179 create_exception!(adblock, OptimizedFilterExistence, BlockerException); 180 create_exception!(adblock, BadFilterAddUnsupported, BlockerException); 181 create_exception!(adblock, FilterExists, BlockerException); 182 183 impl From<BlockerError> for PyErr { 184 fn from(err: BlockerError) -> Self { 185 let msg = format!("{:?}", err); 186 match err { 187 BlockerError::SerializationError => Self::new::<SerializationError, _>(msg), 188 BlockerError::DeserializationError => Self::new::<DeserializationError, _>(msg), 189 BlockerError::OptimizedFilterExistence => Self::new::<OptimizedFilterExistence, _>(msg), 190 BlockerError::BadFilterAddUnsupported => Self::new::<BadFilterAddUnsupported, _>(msg), 191 BlockerError::FilterExists => Self::new::<FilterExists, _>(msg), 192 } 193 } 194 } 195 196 197 fn filter_format_from_string(filter_format: &str) -> PyResult<FilterFormat> { 198 match filter_format { 199 "standard" => Ok(FilterFormat::Standard), 200 "hosts" => Ok(FilterFormat::Hosts), 201 _ => Err(PyErr::new::<AdblockException, _>( 202 "Invalid FilterFormat value", 203 )), 204 } 205 } 206 207 fn rule_types_from_string(rule_types: &str) -> PyResult<RuleTypes> { 208 match rule_types { 209 "all" => Ok(RuleTypes::All), 210 "networkonly" => Ok(RuleTypes::NetworkOnly), 211 "cosmeticonly" => Ok(RuleTypes::CosmeticOnly), 212 _ => Err(PyErr::new::<AdblockException, _>("Invalid RuleTypes value")), 213 } 214 } 215 216 /// Manages a set of rules to be added to an Engine. 217 /// 218 /// To be able to efficiently handle special options like $badfilter, and to 219 /// allow optimizations, all rules must be available when the Engine is first 220 /// created. FilterSet allows assembling a compound list from multiple 221 /// different sources before compiling the rules into an Engine. 222 #[pyclass] 223 #[derive(Clone)] 224 pub struct FilterSet { 225 filter_set: RustFilterSet, 226 debug: bool, 227 } 228 229 #[pymethods] 230 impl FilterSet { 231 /// Creates a new `FilterSet`. The `debug` argument specifies whether or 232 /// not to save information about the original raw filter rules alongside 233 /// the more compact internal representation. If enabled, this information 234 /// will be passed to the corresponding Engine. 235 #[new] 236 #[pyo3(signature = (debug = false))] 237 pub fn new(debug: bool) -> Self { 238 Self { 239 filter_set: RustFilterSet::new(debug), 240 debug, 241 } 242 } 243 244 /// Adds the contents of an entire filter list to this FilterSet. Filters 245 /// that cannot be parsed successfully are ignored. 246 /// 247 /// The format is a string containing either "standard" (ABP/uBO-style) 248 /// or "hosts". 249 #[pyo3(signature = (filter_list, format = "standard", rule_types = "all"))] 250 pub fn add_filter_list( 251 &mut self, 252 filter_list: &str, 253 format: &str, 254 rule_types: &str, 255 ) -> PyResult<()> { 256 let filter_format = filter_format_from_string(format)?; 257 let rule_types = rule_types_from_string(rule_types)?; 258 self.filter_set.add_filter_list( 259 filter_list, 260 ParseOptions { 261 format: filter_format, 262 rule_types, 263 permissions: PermissionMask::default(), 264 }, 265 ); 266 Ok(()) 267 } 268 269 /// Adds a collection of filter rules to this FilterSet. Filters that 270 /// cannot be parsed successfully are ignored. 271 /// 272 /// The format is a string containing either "standard" (ABP/uBO-style) 273 /// or "hosts". 274 #[pyo3(signature = (filters, format = "standard", rule_types = "all"))] 275 pub fn add_filters( 276 &mut self, 277 filters: Vec<String>, 278 format: &str, 279 rule_types: &str, 280 ) -> PyResult<()> { 281 let filter_format = filter_format_from_string(format)?; 282 let rule_types = rule_types_from_string(rule_types)?; 283 self.filter_set.add_filters( 284 &filters, 285 ParseOptions { 286 format: filter_format, 287 rule_types, 288 permissions: PermissionMask::default(), 289 }, 290 ); 291 Ok(()) 292 } 293 294 fn __repr__(&self) -> PyResult<String> { 295 Ok(format!("FilterSet(debug={})", self.debug.diy_python_repr())) 296 } 297 } 298 299 /// Contains cosmetic filter information intended to be injected into a 300 /// particular hostname. 301 #[pyclass] 302 pub struct UrlSpecificResources { 303 /// A set of any CSS selector on the page that should be hidden, i.e. 304 /// styled as `{ display: none !important; }`. 305 #[pyo3(get)] 306 pub hide_selectors: HashSet<String>, 307 /// Set of JSON-encoded procedural filters or filters with an action. 308 #[pyo3(get)] 309 pub procedural_actions: HashSet<String>, 310 /// A set of any class or id CSS selectors that should not have generic 311 /// rules applied. 312 // In practice, these should be passed to `class_id_stylesheet` and not 313 // used otherwise. 314 #[pyo3(get)] 315 pub exceptions: HashSet<String>, 316 /// Javascript code for any scriptlets that should be injected into the 317 /// page. 318 #[pyo3(get)] 319 pub injected_script: String, 320 /// `generichide` is set to `True` if there is a corresponding 321 /// `$generichide` exception network filter. If so, the page should not 322 /// query for additional generic rules using hidden_class_id_selectors. 323 #[pyo3(get)] 324 pub generichide: bool, 325 } 326 327 impl From<RustUrlSpecificResources> for UrlSpecificResources { 328 fn from(r: RustUrlSpecificResources) -> Self { 329 Self { 330 hide_selectors: r.hide_selectors, 331 procedural_actions: r.procedural_actions, 332 exceptions: r.exceptions, 333 injected_script: r.injected_script, 334 generichide: r.generichide, 335 } 336 } 337 } 338 339 #[pymethods] 340 impl UrlSpecificResources { 341 fn __repr__(&self) -> PyResult<String> { 342 Ok(format!( 343 "UrlSpecificResources<{} hide selectors, {} procedural actions, {} exceptions, injected_javascript={}, generichide={}>", 344 self.hide_selectors.len(), 345 self.procedural_actions.len(), 346 self.exceptions.len(), 347 self.injected_script.diy_python_repr(), 348 self.generichide.diy_python_repr(), 349 )) 350 } 351 } 352 353 /// The main object featured in this library. This object holds the adblocker's 354 /// state, and can be queried to see if a given request should be blocked or 355 /// not. 356 /// 357 /// # Request types 358 /// A few of `Engine`'s methods have a field specifying a "resource type", 359 /// valid examples are: 360 /// * `beacon` 361 /// * `csp_report` 362 /// * `document` 363 /// * `font` 364 /// * `media` 365 /// * `object` 366 /// * `script` 367 /// * `stylesheet` 368 /// * and et cetera... 369 /// See the [Mozilla Web Documentation][1] for more info. 370 /// 371 /// [1]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType 372 #[pyclass] 373 pub struct Engine { 374 engine: RustEngine, 375 optimize: bool, 376 } 377 378 #[pymethods] 379 impl Engine { 380 /// Create a new adblocking engine 381 #[new] 382 #[pyo3(signature = (filter_set, optimize = true))] 383 pub fn new(filter_set: FilterSet, optimize: bool) -> Self { 384 let engine = RustEngine::from_filter_set(filter_set.filter_set, optimize); 385 Self { engine, optimize } 386 } 387 388 /// Check if the given `url`—pointing to a resource of type `request_type`— 389 /// is blocked, assuming the request is made from the given `source_url`. 390 /// Returns an object of type `BlockerResult`. 391 /// 392 /// # Arguments 393 /// * `url` - The URL of the request to check 394 /// * `source_url` - The URL from where the request is made 395 /// * `request_type` - The resource type that the request points to 396 pub fn check_network_urls( 397 &self, 398 url: &str, 399 source_url: &str, 400 request_type: &str, 401 ) -> BlockerResult { 402 match Request::new(url, source_url, request_type) { 403 Ok(request) => { 404 let blocker_result = self.engine.check_network_request(&request); 405 blocker_result.into() 406 } 407 Err(_) => BlockerResult { 408 matched: false, 409 important: false, 410 redirect: None, 411 rewritten_url: None, 412 exception: None, 413 filter: None, 414 } 415 } 416 } 417 418 /// Check if a request should be blocked based on the given parameters. 419 /// 420 /// # Arguments 421 /// * `url` - The URL of the request to check 422 /// * `hostname` - The given `url`'s hostname 423 /// * `source_hostname` - The hostname of the source URL. 424 /// * `request_type` - The resource type that the request points to 425 /// * `third_party_request` - Is the given request to a third-party? Here, 426 /// `None` can be given and the engine will figure it out based on the 427 /// `hostname` and `source_hostname`. 428 #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None))] 429 pub fn check_network_urls_with_hostnames( 430 &self, 431 url: &str, 432 hostname: &str, 433 source_hostname: &str, 434 request_type: &str, 435 third_party_request: Option<bool>, 436 ) -> BlockerResult { 437 let third_party = third_party_request.unwrap_or_else(|| { 438 hostname != source_hostname 439 }); 440 let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party); 441 let blocker_result = self.engine.check_network_request(&request); 442 blocker_result.into() 443 } 444 445 /// Check if a request should be blocked based on the given parameters. 446 /// 447 /// # Arguments 448 /// * `url` - The URL of the request to check 449 /// * `hostname` - The given `url`'s hostname 450 /// * `source_hostname` - The hostname of the source URL. 451 /// * `request_type` - The resource type that the request points to 452 /// * `third_party_request` - Is the given request to a third-party? Here, 453 /// `None` can be given and the engine will figure it out based on the 454 /// `hostname` and `source_hostname`. 455 /// * `previously_matched_rule` - Return a match as long as there are no 456 /// exceptions 457 /// * `force_check_exceptions` - Check exceptions even if no other rule matches 458 #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None, previously_matched_rule = false, force_check_exceptions = false))] 459 #[allow(clippy::too_many_arguments)] 460 pub fn check_network_urls_with_hostnames_subset( 461 &self, 462 url: &str, 463 hostname: &str, 464 source_hostname: &str, 465 request_type: &str, 466 third_party_request: Option<bool>, 467 previously_matched_rule: bool, 468 force_check_exceptions: bool, 469 ) -> BlockerResult { 470 let third_party = third_party_request.unwrap_or_else(|| { 471 hostname != source_hostname 472 }); 473 let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party); 474 let blocker_result = self.engine.check_network_request_subset( 475 &request, 476 previously_matched_rule, 477 force_check_exceptions, 478 ); 479 blocker_result.into() 480 } 481 482 /// Sets this engine's resources to additionally include `resource`. 483 /// 484 /// # Arguments 485 /// * `name`: Represents the primary name of the resource, often a filename 486 /// * `content_type`: How to interpret the resource data within `content`. 487 /// Use `"template"` if wanting to specify a template resource type. 488 /// * `content`: The resource data, encoded using standard base64 configuration 489 /// * `aliases`: List of aliases for the resource 490 #[pyo3(signature = (name, content_type, content, aliases = None))] 491 pub fn add_resource( 492 &mut self, 493 name: &str, 494 content_type: &str, 495 content: &str, 496 aliases: Option<Vec<String>>, 497 ) -> PyResult<()> { 498 let resource = Resource { 499 name: name.to_string(), 500 aliases: aliases.unwrap_or_default(), 501 kind: match content_type { 502 "template" => ResourceType::Template, 503 _ => ResourceType::Mime(MimeType::from(std::borrow::Cow::from( 504 content_type.to_string(), 505 ))), 506 }, 507 content: content.to_string(), 508 dependencies: Vec::new(), 509 permission: PermissionMask::default(), 510 }; 511 512 self.engine.use_resources(std::iter::once(resource)); 513 Ok(()) 514 } 515 516 /// Serialize this blocking engine to bytes. They can then be deserialized 517 /// using `deserialize()` to get the same engine again. 518 pub fn serialize<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyBytes>> { 519 let bytes = self.engine.serialize(); 520 let py_bytes = PyBytes::new_bound(py, &bytes); 521 Ok(py_bytes) 522 } 523 524 /// Serialize this blocking engine to a file. The file can then be 525 /// deserialized using `deserialize_from_file()` to get the same engine 526 /// again. 527 pub fn serialize_to_file(&self, file: &str) -> PyResult<()> { 528 let data = self.engine.serialize(); 529 let mut fd = fs::OpenOptions::new() 530 .create(true) 531 .truncate(true) 532 .write(true) 533 .open(file)?; 534 fd.write_all(&data)?; 535 Ok(()) 536 } 537 538 /// Deserialize a blocking engine from bytes produced with `serialize()`. 539 pub fn deserialize(&mut self, serialized: &[u8]) -> PyResult<()> { 540 self.engine.deserialize(serialized).map_err(|_| { 541 DeserializationError::new_err("Failed to deserialize engine data") 542 }) 543 } 544 545 /// Deserialize a blocking engine from file produced with 546 /// `serialize_to_file()`. 547 pub fn deserialize_from_file(&mut self, file: &str) -> PyResult<()> { 548 let mut fd = fs::File::open(file)?; 549 let mut data: Vec<u8> = Vec::new(); 550 fd.read_to_end(&mut data)?; 551 self.deserialize(&data) 552 } 553 554 /// Checks if the given filter exists in the blocking engine. 555 /// Note: This method has been removed in the latest version of adblock-rust. 556 /// It will always return false. 557 pub fn filter_exists(&self, _filter: &str) -> bool { 558 // This method no longer exists in the adblock-rust API 559 false 560 } 561 562 /// Sets this engine's tags to be _only_ the ones provided in tags. 563 /// 564 /// Tags can be used to cheaply enable or disable network rules with a 565 /// corresponding $tag option. 566 pub fn use_tags(&mut self, tags: Vec<String>) { 567 let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); 568 self.engine.use_tags(&tag_refs); 569 } 570 571 /// Sets this engine's tags to additionally include the ones provided in 572 /// tags. 573 /// 574 /// Tags can be used to cheaply enable or disable network rules with a 575 /// corresponding $tag option. 576 pub fn enable_tags(&mut self, tags: Vec<String>) { 577 let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); 578 self.engine.enable_tags(&tag_refs); 579 } 580 581 /// Sets this engine's tags to no longer include the ones provided in 582 /// tags. 583 /// 584 /// Tags can be used to cheaply enable or disable network rules with a 585 /// corresponding $tag option. 586 pub fn disable_tags(&mut self, tags: Vec<String>) { 587 let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); 588 self.engine.disable_tags(&tag_refs); 589 } 590 591 /// Checks if a given tag exists in this engine. 592 /// 593 /// Tags can be used to cheaply enable or disable network rules with a 594 /// corresponding $tag option. 595 pub fn tag_exists(&self, tag: &str) -> bool { 596 self.engine.tag_exists(tag) 597 } 598 599 /// Returns a set of cosmetic filter resources required for a particular 600 /// url. Once this has been called, all CSS ids and classes on a 601 /// page should be passed to hidden_class_id_selectors to obtain any 602 /// stylesheets consisting of generic rules. 603 pub fn url_cosmetic_resources(&self, url: &str) -> UrlSpecificResources { 604 self.engine.url_cosmetic_resources(url).into() 605 } 606 607 /// If any of the provided CSS classes or ids could cause a certain generic 608 /// CSS hide rule (i.e. `{ display: none !important; }`) to be required, this 609 /// method will return a list of CSS selectors corresponding to rules 610 /// referencing those classes or ids, provided that the corresponding rules 611 /// are not excepted. 612 /// 613 /// Exceptions should be passed directly from UrlSpecificResources. 614 pub fn hidden_class_id_selectors( 615 &self, 616 classes: Vec<String>, 617 ids: Vec<String>, 618 exceptions: HashSet<String>, 619 ) -> PyResult<Vec<String>> { 620 Ok(self 621 .engine 622 .hidden_class_id_selectors(&classes, &ids, &exceptions)) 623 } 624 625 fn __repr__(&self) -> PyResult<String> { 626 Ok(format!( 627 "Engine<optimize={}>", 628 self.optimize.diy_python_repr() 629 )) 630 } 631 } 632 633 /// PyO3 doesn't offer the ability to get the Python representation of a Rust 634 /// object, so we make our own trait. 635 trait DiyPythonRepr { 636 fn diy_python_repr(&self) -> String; 637 } 638 639 impl<T> DiyPythonRepr for Option<T> 640 where 641 T: DiyPythonRepr, 642 { 643 fn diy_python_repr(&self) -> String { 644 match self { 645 None => "None".to_owned(), 646 Some(x) => x.diy_python_repr(), 647 } 648 } 649 } 650 651 impl DiyPythonRepr for String { 652 fn diy_python_repr(&self) -> String { 653 let mut res = format!("{:?}", self); 654 // This is safe to do since we know that `res` will always be of 655 // length >= 2. 656 res.replace_range(0..1, "'"); 657 res.replace_range(res.len() - 1..res.len(), "'"); 658 res 659 } 660 } 661 662 impl DiyPythonRepr for bool { 663 fn diy_python_repr(&self) -> String { 664 if *self { 665 "True".to_owned() 666 } else { 667 "False".to_owned() 668 } 669 } 670 }