deb822-lossless-0.1.6/.cargo_vcs_info.json0000644000000001360000000000100136730ustar { "git": { "sha1": "bbefef687b5ff5bb899f4fd59914d1fea444237c" }, "path_in_vcs": "" }deb822-lossless-0.1.6/.github/workflows/rust.yml000064400000000000000000000005001046102023000175730ustar 00000000000000name: Rust on: push: branches: [ "master" ] pull_request: branches: [ "master" ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose deb822-lossless-0.1.6/.gitignore000064400000000000000000000000131046102023000144450ustar 00000000000000/target *~ deb822-lossless-0.1.6/Cargo.lock0000644000000101560000000000100116510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "deb822-lossless" version = "0.1.6" dependencies = [ "regex", "rowan", "serde", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rowan" version = "0.15.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "906057e449592587bf6724f00155bf82a6752c868d78a8fb3aa41f4e6357cfe8" dependencies = [ "countme", "hashbrown", "memoffset", "rustc-hash", "text-size", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "serde" version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" deb822-lossless-0.1.6/Cargo.toml0000644000000017500000000000100116740ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "deb822-lossless" version = "0.1.6" authors = ["Jelmer Vernooij "] description = "A lossless parser for deb822 files" homepage = "https://github.com/jelmer/deb822-lossless" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/jelmer/deb822-lossless" [dependencies.regex] version = "1" [dependencies.rowan] version = "0.15.11" [dependencies.serde] version = "1" features = ["derive"] optional = true [features] default = ["serde"] serde = ["dep:serde"] deb822-lossless-0.1.6/Cargo.toml.orig000064400000000000000000000013131046102023000153500ustar 00000000000000[package] name = "deb822-lossless" authors = ["Jelmer Vernooij "] version = { workspace = true } edition = "2021" license = "Apache-2.0" description = "A lossless parser for deb822 files" repository = { workspace = true } homepage = { workspace = true } [workspace] members = ["debian-control", "debian-copyright", "dep3"] [workspace.package] version = "0.1.6" repository = "https://github.com/jelmer/deb822-lossless" homepage = "https://github.com/jelmer/deb822-lossless" [workspace.dependencies] rowan = "0.15.11" [dependencies] regex = "1" rowan = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } [features] default = ["serde"] serde = ["dep:serde"] deb822-lossless-0.1.6/README.md000064400000000000000000000002511046102023000137400ustar 00000000000000Lossless parser for deb822 style files ====================================== This crate contains lossless parsers and editors for RFC822 style file as used in Debian. deb822-lossless-0.1.6/disperse.conf000064400000000000000000000000461046102023000151500ustar 00000000000000timeout_days: 5 tag_name: "v$VERSION" deb822-lossless-0.1.6/src/lex.rs000064400000000000000000000137351046102023000144210ustar 00000000000000use crate::SyntaxKind; use std::iter::Peekable; use std::str::Chars; pub struct Lexer<'a> { input: Peekable>, start_of_line: bool, indent: usize, } impl<'a> Lexer<'a> { pub fn new(input: &'a str) -> Self { Lexer { input: input.chars().peekable(), start_of_line: true, indent: 0, } } fn is_whitespace(c: char) -> bool { c == ' ' || c == '\t' } fn is_newline(c: char) -> bool { c == '\n' || c == '\r' } fn is_valid_key_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' } fn read_while(&mut self, predicate: F) -> String where F: Fn(char) -> bool, { let mut result = String::new(); while let Some(&c) = self.input.peek() { if predicate(c) { result.push(c); self.input.next(); } else { break; } } result } fn next_token(&mut self) -> Option<(SyntaxKind, String)> { if let Some(&c) = self.input.peek() { match c { ':' => { self.input.next(); Some((SyntaxKind::COLON, ":".to_owned())) } _ if Self::is_newline(c) => { self.input.next(); self.start_of_line = true; self.indent = 0; Some((SyntaxKind::NEWLINE, c.to_string())) } _ if Self::is_whitespace(c) => { let whitespace = self.read_while(Self::is_whitespace); if self.start_of_line { self.indent = whitespace.len(); Some((SyntaxKind::INDENT, whitespace)) } else { Some((SyntaxKind::WHITESPACE, whitespace)) } } '#' if self.start_of_line => { self.input.next(); let comment = self.read_while(|c| c != '\n' && c != '\r'); self.start_of_line = true; Some((SyntaxKind::COMMENT, format!("#{}", comment))) } _ if Self::is_valid_key_char(c) && self.start_of_line && self.indent == 0 => { let key = self.read_while(Self::is_valid_key_char); self.start_of_line = false; Some((SyntaxKind::KEY, key)) } _ if !self.start_of_line || self.indent > 0 => { let value = self.read_while(|c| !Self::is_newline(c)); Some((SyntaxKind::VALUE, value)) } _ => { self.input.next(); Some((SyntaxKind::ERROR, c.to_string())) } } } else { None } } } impl Iterator for Lexer<'_> { type Item = (crate::SyntaxKind, String); fn next(&mut self) -> Option { self.next_token() } } pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> { let mut lexer = Lexer::new(input); lexer.by_ref().collect::>() } #[cfg(test)] mod tests { use crate::SyntaxKind::*; #[test] fn test_empty() { assert_eq!(super::lex(""), vec![]); } #[test] fn test_simple() { assert_eq!( super::lex( r#"Source: syncthing-gtk Maintainer: Jelmer Vernooij Section: net # This is the first binary package: Package: syncthing-gtk Architecture: all Depends: foo, bar, blah (= 1.0) Description: a package with a loooong . long . description "# ) .iter() .map(|(kind, text)| (*kind, text.as_str())) .collect::>(), vec![ (KEY, "Source"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "syncthing-gtk"), (NEWLINE, "\n"), (KEY, "Maintainer"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "Jelmer Vernooij "), (NEWLINE, "\n"), (KEY, "Section"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "net "), (NEWLINE, "\n"), (NEWLINE, "\n"), (COMMENT, "# This is the first binary package:"), (NEWLINE, "\n"), (NEWLINE, "\n"), (KEY, "Package"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "syncthing-gtk"), (NEWLINE, "\n"), (KEY, "Architecture"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "all"), (NEWLINE, "\n"), (KEY, "Depends"), (COLON, ":"), (WHITESPACE, " "), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "foo,"), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "bar,"), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "blah (= 1.0)"), (NEWLINE, "\n"), (KEY, "Description"), (COLON, ":"), (WHITESPACE, " "), (VALUE, "a package"), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "with a loooong"), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "."), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "long"), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "."), (NEWLINE, "\n"), (INDENT, " "), (VALUE, "description"), (NEWLINE, "\n") ] ); } } deb822-lossless-0.1.6/src/lib.rs000064400000000000000000000435571046102023000144040ustar 00000000000000//! Lossless parser for deb822 style files. //! //! This parser can be used to parse files in the deb822 format, while preserving //! all whitespace and comments. It is based on the [rowan] library, which is a //! lossless parser library for Rust. //! //! Once parsed, the file can be traversed or modified, and then written back to //! a file. mod lex; use crate::lex::lex; use rowan::ast::AstNode; use std::str::FromStr; /// Let's start with defining all kinds of tokens and /// composite nodes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] #[repr(u16)] pub enum SyntaxKind { KEY = 0, VALUE, COLON, INDENT, NEWLINE, WHITESPACE, // whitespaces is explicit COMMENT, // comments ERROR, // as well as errors // composite nodes ROOT, // The entire file PARAGRAPH, // A deb822 paragraph ENTRY, // A single key-value pair } use SyntaxKind::*; /// Convert our `SyntaxKind` into the rowan `SyntaxKind`. impl From for rowan::SyntaxKind { fn from(kind: SyntaxKind) -> Self { Self(kind as u16) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParseError(Vec); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for err in &self.0 { writeln!(f, "{}", err)?; } Ok(()) } } impl std::error::Error for ParseError {} /// Second, implementing the `Language` trait teaches rowan to convert between /// these two SyntaxKind types, allowing for a nicer SyntaxNode API where /// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Lang {} impl rowan::Language for Lang { type Kind = SyntaxKind; fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { unsafe { std::mem::transmute::(raw.0) } } fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { kind.into() } } /// GreenNode is an immutable tree, which is cheap to change, /// but doesn't contain offsets and parent pointers. use rowan::GreenNode; /// You can construct GreenNodes by hand, but a builder /// is helpful for top-down parsers: it maintains a stack /// of currently in-progress nodes use rowan::GreenNodeBuilder; /// The parse results are stored as a "green tree". /// We'll discuss working with the results later struct Parse { green_node: GreenNode, #[allow(unused)] errors: Vec, } fn parse(text: &str) -> Parse { struct Parser { /// input tokens, including whitespace, /// in *reverse* order. tokens: Vec<(SyntaxKind, String)>, /// the in-progress tree. builder: GreenNodeBuilder<'static>, /// the list of syntax errors we've accumulated /// so far. errors: Vec, } impl Parser { fn parse_entry(&mut self) { self.builder.start_node(ENTRY.into()); if self.current() == Some(KEY) { self.bump(); self.skip_ws(); } else { self.builder.start_node(ERROR.into()); self.bump(); self.errors.push("expected key".to_string()); self.builder.finish_node(); } if self.current() == Some(COLON) { self.bump(); self.skip_ws(); } else { self.builder.start_node(ERROR.into()); self.bump(); self.errors.push("expected ':'".to_string()); self.builder.finish_node(); } loop { while self.current() == Some(WHITESPACE) || self.current() == Some(VALUE) { self.bump(); } match self.current() { None => { break; } Some(NEWLINE) => { self.bump(); } Some(_) => { self.builder.start_node(ERROR.into()); self.bump(); self.errors.push("expected newline".to_string()); self.builder.finish_node(); } } if self.current() == Some(INDENT) { self.bump(); self.skip_ws(); } else { break; } } self.builder.finish_node(); } fn parse_paragraph(&mut self) { self.builder.start_node(PARAGRAPH.into()); while self.current() != Some(NEWLINE) && self.current().is_some() { self.parse_entry(); } self.builder.finish_node(); } fn parse(mut self) -> Parse { // Make sure that the root node covers all source self.builder.start_node(ROOT.into()); while self.current().is_some() { self.skip_ws_and_newlines(); self.parse_paragraph(); } // Don't forget to eat *trailing* whitespace self.skip_ws_and_newlines(); // Close the root node. self.builder.finish_node(); // Turn the builder into a GreenNode Parse { green_node: self.builder.finish(), errors: self.errors, } } /// Advance one token, adding it to the current branch of the tree builder. fn bump(&mut self) { let (kind, text) = self.tokens.pop().unwrap(); self.builder.token(kind.into(), text.as_str()); } /// Peek at the first unprocessed token fn current(&self) -> Option { self.tokens.last().map(|(kind, _)| *kind) } fn skip_ws(&mut self) { while self.current() == Some(WHITESPACE) || self.current() == Some(COMMENT) { self.bump() } } fn skip_ws_and_newlines(&mut self) { while self.current() == Some(WHITESPACE) || self.current() == Some(COMMENT) || self.current() == Some(NEWLINE) { self.bump() } } } let mut tokens = lex(text); tokens.reverse(); Parser { tokens, builder: GreenNodeBuilder::new(), errors: Vec::new(), } .parse() } /// To work with the parse results we need a view into the /// green tree - the Syntax tree. /// It is also immutable, like a GreenNode, /// but it contains parent pointers, offsets, and /// has identity semantics. type SyntaxNode = rowan::SyntaxNode; #[allow(unused)] type SyntaxToken = rowan::SyntaxToken; #[allow(unused)] type SyntaxElement = rowan::NodeOrToken; impl Parse { fn syntax(&self) -> SyntaxNode { SyntaxNode::new_root(self.green_node.clone()) } fn root(&self) -> Deb822 { Deb822::cast(self.syntax()).unwrap() } } macro_rules! ast_node { ($ast:ident, $kind:ident) => { #[derive(PartialEq, Eq, Hash)] #[repr(transparent)] pub struct $ast(SyntaxNode); impl $ast { #[allow(unused)] fn cast(node: SyntaxNode) -> Option { if node.kind() == $kind { Some(Self(node)) } else { None } } } impl AstNode for $ast { type Language = Lang; fn can_cast(kind: SyntaxKind) -> bool { kind == $kind } fn cast(syntax: SyntaxNode) -> Option { Self::cast(syntax) } fn syntax(&self) -> &SyntaxNode { &self.0 } } impl ToString for $ast { fn to_string(&self) -> String { self.0.text().to_string() } } }; } ast_node!(Deb822, ROOT); ast_node!(Paragraph, PARAGRAPH); ast_node!(Entry, ENTRY); impl Default for Deb822 { fn default() -> Self { Self::new() } } impl Deb822 { pub fn new() -> Deb822 { let mut builder = GreenNodeBuilder::new(); builder.start_node(ROOT.into()); builder.finish_node(); Deb822(SyntaxNode::new_root(builder.finish()).clone_for_update()) } /// Returns an iterator over all paragraphs in the file. pub fn paragraphs(&self) -> impl Iterator { self.0.children().filter_map(Paragraph::cast) } pub fn add_paragraph(&mut self) -> Paragraph { let paragraph = Paragraph::new(); let mut to_insert = vec![]; if self.0.children().count() > 0 { let mut builder = GreenNodeBuilder::new(); builder.token(NEWLINE.into(), "\n"); to_insert.push(SyntaxNode::new_root(builder.finish()).clone_for_update().into()); } to_insert.push(paragraph.0.clone().into()); self.0.splice_children( self.0.children().count()..self.0.children().count(), to_insert ); paragraph } } impl Paragraph { pub fn new() -> Paragraph { let mut builder = GreenNodeBuilder::new(); builder.start_node(PARAGRAPH.into()); builder.finish_node(); Paragraph(SyntaxNode::new_root(builder.finish()).clone_for_update()) } /// Returns the value of the given key in the paragraph. pub fn get(&self, key: &str) -> Option { self.entries() .find(|e| e.key().as_deref() == Some(key)) .map(|e| e.value()) } pub fn contains_key(&self, key: &str) -> bool { self.get(key).is_some() } /// Returns an iterator over all entries in the paragraph. fn entries(&self) -> impl Iterator + '_ { self.0.children().filter_map(Entry::cast) } pub fn items(&self) -> impl Iterator + '_ { self.entries() .filter_map(|e| e.key().map(|k| (k, e.value()))) } pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator + '_ { self.items().filter_map(move |(k, v)| if k.as_str() == key { Some(v) } else { None }) } pub fn contains(&self, key: &str) -> bool { self.get_all(key).any(|_| true) } pub fn keys(&self) -> impl Iterator + '_ { self.entries().filter_map(|e| e.key()) } pub fn remove(&mut self, key: &str) { for mut entry in self.entries() { if entry.key().as_deref() == Some(key) { entry.detach(); } } } pub fn insert(&mut self, key: &str, value: &str) { for mut entry in self.entries() { if entry.key().as_deref() == Some(key) { entry.set_value(value); return; } } let entry = Entry::new(key, value); self.0.splice_children( self.0.children().count()..self.0.children().count(), vec![entry.0.clone_for_update().into()], ); } pub fn rename(&mut self, old_key: &str, new_key: &str) { for mut entry in self.entries() { if entry.key().as_deref() == Some(old_key) { entry.set_key(new_key); } } } } impl Default for Paragraph { fn default() -> Self { Self::new() } } impl std::str::FromStr for Paragraph { type Err = ParseError; fn from_str(text: &str) -> Result { let deb822 = Deb822::from_str(text)?; let mut paragraphs = deb822.paragraphs(); paragraphs.next().ok_or_else(||ParseError(vec!["no paragraphs".to_string()])) } } impl Entry { pub fn new(key: &str, value: &str) -> Entry { let mut builder = GreenNodeBuilder::new(); builder.start_node(ENTRY.into()); builder.token(KEY.into(), key); builder.token(COLON.into(), ":"); builder.token(WHITESPACE.into(), " "); builder.token(VALUE.into(), value); builder.token(NEWLINE.into(), "\n"); builder.finish_node(); Entry(SyntaxNode::new_root(builder.finish())) } pub fn key(&self) -> Option { self.0 .children_with_tokens() .filter_map(|it| it.into_token()) .find(|it| it.kind() == KEY) .map(|it| it.text().to_string()) } pub fn value(&self) -> String { self.0 .children_with_tokens() .filter_map(|it| it.into_token()) .filter(|it| it.kind() == VALUE) .map(|it| it.text().to_string()) .collect::>() .join("\n") } pub fn set_key(&mut self, _key: &str) { todo!(); } pub fn set_value(&mut self, _value: &str) { todo!(); } pub fn detach(&mut self) { self.0.detach(); } } impl FromStr for Deb822 { type Err = ParseError; fn from_str(s: &str) -> Result { let parsed = parse(s); if parsed.errors.is_empty() { Ok(parsed.root().clone_for_update()) } else { Err(ParseError(parsed.errors)) } } } #[test] fn test_parse_simple() { const CONTROLV1: &str = r#"Source: foo Maintainer: Foo Bar Section: net # This is a comment Package: foo Architecture: all Depends: bar, blah Description: This is a description And it is . multiple lines "#; let parsed = parse(CONTROLV1); let node = parsed.syntax(); assert_eq!( format!("{:#?}", node), r###"ROOT@0..203 PARAGRAPH@0..63 ENTRY@0..12 KEY@0..6 "Source" COLON@6..7 ":" WHITESPACE@7..8 " " VALUE@8..11 "foo" NEWLINE@11..12 "\n" ENTRY@12..50 KEY@12..22 "Maintainer" COLON@22..23 ":" WHITESPACE@23..24 " " VALUE@24..49 "Foo Bar ::new()); let root = parsed.root(); assert_eq!(root.paragraphs().count(), 2); let source = root.paragraphs().next().unwrap(); assert_eq!( source.keys().collect::>(), vec!["Source", "Maintainer", "Section"] ); assert_eq!(source.get("Source").as_deref(), Some("foo")); assert_eq!( source.get("Maintainer").as_deref(), Some("Foo Bar ") ); assert_eq!(source.get("Section").as_deref(), Some("net")); assert_eq!( source.items().collect::>(), vec![ ("Source".into(), "foo".into()), ("Maintainer".into(), "Foo Bar ".into()), ("Section".into(), "net".into()), ] ); let binary = root.paragraphs().nth(1).unwrap(); assert_eq!( binary.keys().collect::>(), vec!["Package", "Architecture", "Depends", "Description"] ); assert_eq!(binary.get("Package").as_deref(), Some("foo")); assert_eq!(binary.get("Architecture").as_deref(), Some("all")); assert_eq!(binary.get("Depends").as_deref(), Some("bar,\nblah")); assert_eq!( binary.get("Description").as_deref(), Some("This is a description\nAnd it is\n.\nmultiple\nlines") ); assert_eq!(node.text(), CONTROLV1); } #[cfg(test)] mod tests { #[test] fn test_parse() { let d: super::Deb822 = r#"Source: foo Maintainer: Foo Bar Section: net Package: foo Architecture: all Depends: libc6 Description: This is a description With details "# .parse() .unwrap(); let mut ps = d.paragraphs(); let p = ps.next().unwrap(); assert_eq!(p.get("Source").as_deref(), Some("foo")); assert_eq!( p.get("Maintainer").as_deref(), Some("Foo Bar ") ); assert_eq!(p.get("Section").as_deref(), Some("net")); let b = ps.next().unwrap(); assert_eq!(b.get("Package").as_deref(), Some("foo")); } #[test] fn test_modify() { let d: super::Deb822 = r#"Source: foo Maintainer: Foo Bar Section: net Package: foo Architecture: all Depends: libc6 Description: This is a description With details "# .parse() .unwrap(); let mut ps = d.paragraphs(); let mut p = ps.next().unwrap(); p.insert("Foo", "Bar"); p.remove("Section"); p.remove("Nonexistant"); assert_eq!(p.get("Foo").as_deref(), Some("Bar")); assert_eq!( p.to_string(), r#"Source: foo Maintainer: Foo Bar Foo: Bar "# ); } } deb822-lossless-0.1.6/src/main.rs000064400000000000000000000000551046102023000145440ustar 00000000000000fn main() { println!("Hello, world!"); }