use crate::lex::lex;
use crate::types::{
ComponentType, Compression, GitExport, GitMode, Mode, PgpMode, Pretty, SearchMode,
};
use crate::SyntaxKind;
use crate::SyntaxKind::*;
use crate::DEFAULT_VERSION;
use std::io::Read;
use std::marker::PhantomData;
use std::str::FromStr;
#[cfg(test)]
use crate::types::VersionPolicy;
pub(crate) fn watch_option_to_key(option: &crate::types::WatchOption) -> &'static str {
use crate::types::WatchOption;
match option {
WatchOption::Component(_) => "component",
WatchOption::Compression(_) => "compression",
WatchOption::UserAgent(_) => "user-agent",
WatchOption::Pagemangle(_) => "pagemangle",
WatchOption::Uversionmangle(_) => "uversionmangle",
WatchOption::Dversionmangle(_) => "dversionmangle",
WatchOption::Dirversionmangle(_) => "dirversionmangle",
WatchOption::Oversionmangle(_) => "oversionmangle",
WatchOption::Downloadurlmangle(_) => "downloadurlmangle",
WatchOption::Pgpsigurlmangle(_) => "pgpsigurlmangle",
WatchOption::Filenamemangle(_) => "filenamemangle",
WatchOption::VersionPolicy(_) => "version-policy",
WatchOption::Searchmode(_) => "searchmode",
WatchOption::Mode(_) => "mode",
WatchOption::Pgpmode(_) => "pgpmode",
WatchOption::Gitexport(_) => "gitexport",
WatchOption::Gitmode(_) => "gitmode",
WatchOption::Pretty(_) => "pretty",
WatchOption::Ctype(_) => "ctype",
WatchOption::Repacksuffix(_) => "repacksuffix",
WatchOption::Unzipopt(_) => "unzipopt",
WatchOption::Script(_) => "script",
WatchOption::Decompress => "decompress",
WatchOption::Bare => "bare",
WatchOption::Repack => "repack",
}
}
pub(crate) fn watch_option_to_value(option: &crate::types::WatchOption) -> String {
use crate::types::WatchOption;
match option {
WatchOption::Component(v) => v.clone(),
WatchOption::Compression(v) => v.to_string(),
WatchOption::UserAgent(v) => v.clone(),
WatchOption::Pagemangle(v) => v.clone(),
WatchOption::Uversionmangle(v) => v.clone(),
WatchOption::Dversionmangle(v) => v.clone(),
WatchOption::Dirversionmangle(v) => v.clone(),
WatchOption::Oversionmangle(v) => v.clone(),
WatchOption::Downloadurlmangle(v) => v.clone(),
WatchOption::Pgpsigurlmangle(v) => v.clone(),
WatchOption::Filenamemangle(v) => v.clone(),
WatchOption::VersionPolicy(v) => v.to_string(),
WatchOption::Searchmode(v) => v.to_string(),
WatchOption::Mode(v) => v.to_string(),
WatchOption::Pgpmode(v) => v.to_string(),
WatchOption::Gitexport(v) => v.to_string(),
WatchOption::Gitmode(v) => v.to_string(),
WatchOption::Pretty(v) => v.to_string(),
WatchOption::Ctype(v) => v.to_string(),
WatchOption::Repacksuffix(v) => v.clone(),
WatchOption::Unzipopt(v) => v.clone(),
WatchOption::Script(v) => v.clone(),
WatchOption::Decompress => String::new(),
WatchOption::Bare => String::new(),
WatchOption::Repack => String::new(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParseError(pub Vec<String>);
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 {}
#[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::<u16, SyntaxKind>(raw.0) }
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.into()
}
}
use rowan::GreenNode;
use rowan::GreenNodeBuilder;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parse<T> {
green: GreenNode,
errors: Vec<String>,
_ty: PhantomData<T>,
}
impl<T> Parse<T> {
pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
Parse {
green,
errors,
_ty: PhantomData,
}
}
pub fn green(&self) -> &GreenNode {
&self.green
}
pub fn errors(&self) -> &[String] {
&self.errors
}
pub fn is_ok(&self) -> bool {
self.errors.is_empty()
}
}
impl Parse<WatchFile> {
pub fn tree(&self) -> WatchFile {
WatchFile::cast(SyntaxNode::new_root_mut(self.green.clone()))
.expect("root node should be a WatchFile")
}
}
unsafe impl<T> Send for Parse<T> {}
unsafe impl<T> Sync for Parse<T> {}
struct InternalParse {
green_node: GreenNode,
errors: Vec<String>,
}
fn is_field_token(kind: Option<SyntaxKind>) -> bool {
matches!(kind, Some(KEY | VALUE | EQUALS | COMMA | QUOTE))
}
fn parse(text: &str) -> InternalParse {
struct Parser {
tokens: Vec<(SyntaxKind, String)>,
builder: GreenNodeBuilder<'static>,
errors: Vec<String>,
}
impl Parser {
fn parse_version(&mut self) -> Option<u32> {
let mut version = None;
if self.tokens.last() == Some(&(KEY, "version".to_string())) {
self.builder.start_node(VERSION.into());
self.bump();
self.skip_ws();
if self.current() != Some(EQUALS) {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
if self.current() != Some(VALUE) {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected value, got {:?}", self.current()));
self.bump();
self.builder.finish_node();
} else if let Some((_, value)) = self.tokens.last() {
let version_str = value;
match version_str.parse() {
Ok(v) => {
version = Some(v);
self.bump();
}
Err(_) => {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("invalid version: {}", version_str));
self.bump();
self.builder.finish_node();
}
}
} else {
self.builder.start_node(ERROR.into());
self.errors.push("expected version value".to_string());
self.builder.finish_node();
}
if self.current() != Some(NEWLINE) {
self.builder.start_node(ERROR.into());
self.errors.push("expected newline".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
self.builder.finish_node();
}
version
}
fn parse_watch_entry(&mut self) -> bool {
loop {
self.skip_ws();
if self.current() == Some(NEWLINE) {
self.bump();
} else {
break;
}
}
if self.current().is_none() {
return false;
}
self.builder.start_node(ENTRY.into());
self.parse_options_list();
for i in 0..4 {
if self.current() == Some(NEWLINE) || self.current().is_none() {
break;
}
if self.current() == Some(CONTINUATION) {
self.bump();
self.skip_ws();
continue;
}
if !matches!(self.current(), Some(KEY | VALUE)) {
self.builder.start_node(ERROR.into());
self.errors.push(format!(
"expected value, got {:?} (i={})",
self.current(),
i
));
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
} else {
let kind = match i {
0 => URL,
1 => MATCHING_PATTERN,
2 => VERSION_POLICY,
3 => SCRIPT,
_ => unreachable!(),
};
self.builder.start_node(kind.into());
while is_field_token(self.current()) {
self.bump();
}
self.builder.finish_node();
}
self.skip_ws();
}
if self.current() != Some(NEWLINE) && self.current().is_some() {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected newline, not {:?}", self.current()));
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
} else if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
true
}
fn parse_option(&mut self, quoted: bool) -> bool {
if self.current().is_none() {
return false;
}
while self.current() == Some(CONTINUATION) {
self.bump();
}
if !quoted && self.current() == Some(WHITESPACE) {
return false;
}
if quoted && self.current() == Some(QUOTE) {
return false;
}
if !quoted && self.current() != Some(KEY) {
return false;
}
self.builder.start_node(OPTION.into());
if self.current() != Some(KEY) {
self.builder.start_node(ERROR.into());
self.errors.push("expected key".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
if self.current() == Some(EQUALS) {
self.bump();
let mut consumed_value = false;
loop {
match self.current() {
Some(KEY) | Some(VALUE) => {
self.bump();
consumed_value = true;
}
Some(EQUALS) if consumed_value => self.bump(),
Some(WHITESPACE) if quoted => {
break;
}
_ => break,
}
}
if !consumed_value {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected value, got {:?}", self.current()));
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
}
} else if self.current() == Some(COMMA) {
} else {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
}
self.builder.finish_node();
true
}
fn parse_options_list(&mut self) {
self.skip_ws();
if self.tokens.last() == Some(&(KEY, "opts".to_string()))
|| self.tokens.last() == Some(&(KEY, "options".to_string()))
{
self.builder.start_node(OPTS_LIST.into());
self.bump();
self.skip_ws();
if self.current() != Some(EQUALS) {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
if self.current().is_some() {
self.bump();
}
self.builder.finish_node();
} else {
self.bump();
}
let quoted = if self.current() == Some(QUOTE) {
self.bump();
true
} else {
false
};
loop {
if quoted {
self.skip_ws();
if self.current() == Some(QUOTE) {
self.bump();
break;
}
}
if !self.parse_option(quoted) {
break;
}
if quoted {
self.skip_ws();
}
if self.current() == Some(COMMA) {
self.builder.start_node(OPTION_SEPARATOR.into());
self.bump();
self.builder.finish_node();
} else if !quoted {
break;
}
}
self.builder.finish_node();
self.skip_ws();
}
}
fn parse(mut self) -> InternalParse {
self.builder.start_node(ROOT.into());
while self.current() == Some(WHITESPACE)
|| self.current() == Some(CONTINUATION)
|| self.current() == Some(COMMENT)
|| self.current() == Some(NEWLINE)
{
self.bump();
}
if let Some(_v) = self.parse_version() {
}
loop {
if !self.parse_watch_entry() {
break;
}
}
self.skip_ws();
if self.current().is_some() {
self.builder.start_node(ERROR.into());
self.errors
.push("unexpected tokens after last entry".to_string());
while self.current().is_some() {
self.bump();
}
self.builder.finish_node();
}
self.builder.finish_node();
InternalParse {
green_node: self.builder.finish(),
errors: self.errors,
}
}
fn bump(&mut self) {
if let Some((kind, text)) = self.tokens.pop() {
self.builder.token(kind.into(), text.as_str());
}
}
fn current(&self) -> Option<SyntaxKind> {
self.tokens.last().map(|(kind, _)| *kind)
}
fn skip_ws(&mut self) {
while self.current() == Some(WHITESPACE)
|| self.current() == Some(CONTINUATION)
|| self.current() == Some(COMMENT)
{
self.bump()
}
}
}
let mut tokens = lex(text);
tokens.reverse();
Parser {
tokens,
builder: GreenNodeBuilder::new(),
errors: Vec::new(),
}
.parse()
}
type SyntaxNode = rowan::SyntaxNode<Lang>;
#[allow(unused)]
type SyntaxToken = rowan::SyntaxToken<Lang>;
#[allow(unused)]
type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
impl InternalParse {
fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root_mut(self.green_node.clone())
}
fn root(&self) -> WatchFile {
WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
}
}
fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
let root = node.ancestors().last().unwrap_or_else(|| node.clone());
let mut line = 0;
let mut last_newline_offset = rowan::TextSize::from(0);
for element in root.preorder_with_tokens() {
if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
if token.text_range().start() >= offset {
break;
}
for (idx, _) in token.text().match_indices('\n') {
line += 1;
last_newline_offset =
token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
}
}
}
let column: usize = (offset - last_newline_offset).into();
(line, column)
}
macro_rules! ast_node {
($ast:ident, $kind:ident) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct $ast(SyntaxNode);
impl $ast {
#[allow(unused)]
fn cast(node: SyntaxNode) -> Option<Self> {
if node.kind() == $kind {
Some(Self(node))
} else {
None
}
}
pub fn line(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).0
}
pub fn column(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).1
}
pub fn line_col(&self) -> (usize, usize) {
line_col_at_offset(&self.0, self.0.text_range().start())
}
}
impl std::fmt::Display for $ast {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.text())
}
}
};
}
ast_node!(WatchFile, ROOT);
ast_node!(Version, VERSION);
ast_node!(Entry, ENTRY);
ast_node!(_Option, OPTION);
ast_node!(Url, URL);
ast_node!(MatchingPattern, MATCHING_PATTERN);
ast_node!(VersionPolicyNode, VERSION_POLICY);
ast_node!(ScriptNode, SCRIPT);
#[derive(Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct OptionList(SyntaxNode);
impl OptionList {
#[allow(unused)]
fn cast(node: SyntaxNode) -> Option<Self> {
if node.kind() == OPTS_LIST {
Some(Self(node))
} else {
None
}
}
pub fn line(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).0
}
pub fn column(&self) -> usize {
line_col_at_offset(&self.0, self.0.text_range().start()).1
}
pub fn line_col(&self) -> (usize, usize) {
line_col_at_offset(&self.0, self.0.text_range().start())
}
}
impl std::fmt::Display for OptionList {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.text())
}
}
impl WatchFile {
pub fn syntax(&self) -> &SyntaxNode {
&self.0
}
pub fn new(version: Option<u32>) -> WatchFile {
let mut builder = GreenNodeBuilder::new();
builder.start_node(ROOT.into());
if let Some(version) = version {
builder.start_node(VERSION.into());
builder.token(KEY.into(), "version");
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), version.to_string().as_str());
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
}
builder.finish_node();
WatchFile(SyntaxNode::new_root_mut(builder.finish()))
}
pub fn version_node(&self) -> Option<Version> {
self.0.children().find_map(Version::cast)
}
pub fn version(&self) -> u32 {
self.version_node()
.map(|it| it.version())
.unwrap_or(DEFAULT_VERSION)
}
pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
self.0.children().filter_map(Entry::cast)
}
pub fn set_version(&mut self, new_version: u32) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(VERSION.into());
builder.token(KEY.into(), "version");
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), new_version.to_string().as_str());
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
let new_version_green = builder.finish();
let new_version_node = SyntaxNode::new_root_mut(new_version_green);
let version_pos = self.0.children().position(|child| child.kind() == VERSION);
if let Some(pos) = version_pos {
self.0
.splice_children(pos..pos + 1, vec![new_version_node.into()]);
} else {
self.0.splice_children(0..0, vec![new_version_node.into()]);
}
}
#[cfg(feature = "discover")]
pub async fn uscan(
&self,
package: impl Fn() -> String + Send + Sync,
) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
let mut all_releases = Vec::new();
for entry in self.entries() {
let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
let releases = parsed_entry.discover(|| package()).await?;
all_releases.push(releases);
}
Ok(all_releases)
}
#[cfg(all(feature = "discover", feature = "blocking"))]
pub fn uscan_blocking(
&self,
package: impl Fn() -> String,
) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
let mut all_releases = Vec::new();
for entry in self.entries() {
let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
let releases = parsed_entry.discover_blocking(|| package())?;
all_releases.push(releases);
}
Ok(all_releases)
}
pub fn add_entry(&mut self, entry: Entry) -> Entry {
let insert_pos = self.0.children_with_tokens().count();
let entry_green = entry.0.green().into_owned();
let new_entry_node = SyntaxNode::new_root_mut(entry_green);
self.0
.splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
Entry::cast(
self.0
.children()
.nth(insert_pos)
.expect("Entry was just inserted"),
)
.expect("Inserted node should be an Entry")
}
pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
let mut buf_reader = std::io::BufReader::new(reader);
let mut content = String::new();
buf_reader
.read_to_string(&mut content)
.map_err(|e| ParseError(vec![e.to_string()]))?;
content.parse()
}
pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
let mut content = String::new();
r.read_to_string(&mut content)?;
let parsed = parse(&content);
Ok(parsed.root())
}
pub fn from_str_relaxed(s: &str) -> Self {
let parsed = parse(s);
parsed.root()
}
}
impl FromStr for WatchFile {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = parse(s);
if parsed.errors.is_empty() {
Ok(parsed.root())
} else {
Err(ParseError(parsed.errors))
}
}
}
pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
let parsed = parse(text);
Parse::new(parsed.green_node, parsed.errors)
}
impl Version {
pub fn version(&self) -> u32 {
self.0
.children_with_tokens()
.find_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE {
token.text().parse().ok()
} else {
None
}
}
_ => None,
})
.unwrap_or(DEFAULT_VERSION)
}
}
#[derive(Debug, Clone, Default)]
pub struct EntryBuilder {
url: Option<String>,
matching_pattern: Option<String>,
version_policy: Option<String>,
script: Option<String>,
opts: std::collections::HashMap<String, String>,
}
impl EntryBuilder {
pub fn new(url: impl Into<String>) -> Self {
EntryBuilder {
url: Some(url.into()),
matching_pattern: None,
version_policy: None,
script: None,
opts: std::collections::HashMap::new(),
}
}
pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
self.matching_pattern = Some(pattern.into());
self
}
pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
self.version_policy = Some(policy.into());
self
}
pub fn script(mut self, script: impl Into<String>) -> Self {
self.script = Some(script.into());
self
}
pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.opts.insert(key.into(), value.into());
self
}
pub fn flag(mut self, key: impl Into<String>) -> Self {
self.opts.insert(key.into(), String::new());
self
}
pub fn build(self) -> Entry {
let url = self.url.expect("URL is required for entry");
let mut builder = GreenNodeBuilder::new();
builder.start_node(ENTRY.into());
if !self.opts.is_empty() {
builder.start_node(OPTS_LIST.into());
builder.token(KEY.into(), "opts");
builder.token(EQUALS.into(), "=");
let mut first = true;
for (key, value) in self.opts.iter() {
if !first {
builder.token(COMMA.into(), ",");
}
first = false;
builder.start_node(OPTION.into());
builder.token(KEY.into(), key);
if !value.is_empty() {
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), value);
}
builder.finish_node();
}
builder.finish_node();
builder.token(WHITESPACE.into(), " ");
}
builder.start_node(URL.into());
builder.token(VALUE.into(), &url);
builder.finish_node();
if let Some(pattern) = self.matching_pattern {
builder.token(WHITESPACE.into(), " ");
builder.start_node(MATCHING_PATTERN.into());
builder.token(VALUE.into(), &pattern);
builder.finish_node();
}
if let Some(policy) = self.version_policy {
builder.token(WHITESPACE.into(), " ");
builder.start_node(VERSION_POLICY.into());
builder.token(VALUE.into(), &policy);
builder.finish_node();
}
if let Some(script_val) = self.script {
builder.token(WHITESPACE.into(), " ");
builder.start_node(SCRIPT.into());
builder.token(VALUE.into(), &script_val);
builder.finish_node();
}
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
Entry(SyntaxNode::new_root_mut(builder.finish()))
}
}
impl Entry {
pub fn syntax(&self) -> &SyntaxNode {
&self.0
}
pub fn builder(url: impl Into<String>) -> EntryBuilder {
EntryBuilder::new(url)
}
pub fn option_list(&self) -> Option<OptionList> {
self.0.children().find_map(OptionList::cast)
}
pub fn get_option(&self, key: &str) -> Option<String> {
self.option_list().and_then(|ol| ol.get_option(key))
}
pub fn has_option(&self, key: &str) -> bool {
self.option_list().is_some_and(|ol| ol.has_option(key))
}
pub fn component(&self) -> Option<String> {
self.get_option("component")
}
pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
self.try_ctype().map_err(|_| ())
}
pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
self.get_option("ctype").map(|s| s.parse()).transpose()
}
pub fn compression(&self) -> Result<Option<Compression>, ()> {
self.try_compression().map_err(|_| ())
}
pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
self.get_option("compression")
.map(|s| s.parse())
.transpose()
}
pub fn repack(&self) -> bool {
self.has_option("repack")
}
pub fn repacksuffix(&self) -> Option<String> {
self.get_option("repacksuffix")
}
pub fn mode(&self) -> Result<Mode, ()> {
self.try_mode().map_err(|_| ())
}
pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
Ok(self
.get_option("mode")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn pretty(&self) -> Result<Pretty, ()> {
self.try_pretty().map_err(|_| ())
}
pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
Ok(self
.get_option("pretty")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn date(&self) -> String {
self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
}
pub fn gitexport(&self) -> Result<GitExport, ()> {
self.try_gitexport().map_err(|_| ())
}
pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
Ok(self
.get_option("gitexport")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn gitmode(&self) -> Result<GitMode, ()> {
self.try_gitmode().map_err(|_| ())
}
pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
Ok(self
.get_option("gitmode")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn pgpmode(&self) -> Result<PgpMode, ()> {
self.try_pgpmode().map_err(|_| ())
}
pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
Ok(self
.get_option("pgpmode")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn searchmode(&self) -> Result<SearchMode, ()> {
self.try_searchmode().map_err(|_| ())
}
pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
Ok(self
.get_option("searchmode")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
pub fn decompress(&self) -> bool {
self.has_option("decompress")
}
pub fn bare(&self) -> bool {
self.has_option("bare")
}
pub fn user_agent(&self) -> Option<String> {
self.get_option("user-agent")
}
pub fn passive(&self) -> Option<bool> {
if self.has_option("passive") || self.has_option("pasv") {
Some(true)
} else if self.has_option("active") || self.has_option("nopasv") {
Some(false)
} else {
None
}
}
pub fn unzipoptions(&self) -> Option<String> {
self.get_option("unzipopt")
}
pub fn dversionmangle(&self) -> Option<String> {
self.get_option("dversionmangle")
.or_else(|| self.get_option("versionmangle"))
}
pub fn dirversionmangle(&self) -> Option<String> {
self.get_option("dirversionmangle")
}
pub fn pagemangle(&self) -> Option<String> {
self.get_option("pagemangle")
}
pub fn uversionmangle(&self) -> Option<String> {
self.get_option("uversionmangle")
.or_else(|| self.get_option("versionmangle"))
}
pub fn versionmangle(&self) -> Option<String> {
self.get_option("versionmangle")
}
pub fn hrefdecode(&self) -> bool {
self.get_option("hrefdecode").is_some()
}
pub fn downloadurlmangle(&self) -> Option<String> {
self.get_option("downloadurlmangle")
}
pub fn filenamemangle(&self) -> Option<String> {
self.get_option("filenamemangle")
}
pub fn pgpsigurlmangle(&self) -> Option<String> {
self.get_option("pgpsigurlmangle")
}
pub fn oversionmangle(&self) -> Option<String> {
self.get_option("oversionmangle")
}
pub fn apply_uversionmangle(
&self,
version: &str,
) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.uversionmangle() {
crate::mangle::apply_mangle(&vm, version)
} else {
Ok(version.to_string())
}
}
pub fn apply_dversionmangle(
&self,
version: &str,
) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.dversionmangle() {
crate::mangle::apply_mangle(&vm, version)
} else {
Ok(version.to_string())
}
}
pub fn apply_oversionmangle(
&self,
version: &str,
) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.oversionmangle() {
crate::mangle::apply_mangle(&vm, version)
} else {
Ok(version.to_string())
}
}
pub fn apply_dirversionmangle(
&self,
version: &str,
) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.dirversionmangle() {
crate::mangle::apply_mangle(&vm, version)
} else {
Ok(version.to_string())
}
}
pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.filenamemangle() {
crate::mangle::apply_mangle(&vm, url)
} else {
Ok(url.to_string())
}
}
pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
if let Some(vm) = self.pagemangle() {
let page_str = String::from_utf8_lossy(page);
let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
Ok(mangled.into_bytes())
} else {
Ok(page.to_vec())
}
}
pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
if let Some(vm) = self.downloadurlmangle() {
crate::mangle::apply_mangle(&vm, url)
} else {
Ok(url.to_string())
}
}
pub fn opts(&self) -> std::collections::HashMap<String, String> {
let mut options = std::collections::HashMap::new();
if let Some(ol) = self.option_list() {
for opt in ol.options() {
let key = opt.key();
let value = opt.value();
if let (Some(key), Some(value)) = (key, value) {
options.insert(key.to_string(), value.to_string());
}
}
}
options
}
fn items(&self) -> impl Iterator<Item = String> + '_ {
self.0.children_with_tokens().filter_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE || token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
SyntaxElement::Node(node) => {
match node.kind() {
URL => Url::cast(node).map(|n| n.url()),
MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
_ => None,
}
}
})
}
pub fn url_node(&self) -> Option<Url> {
self.0.children().find_map(Url::cast)
}
pub fn url(&self) -> String {
self.url_node()
.map(|it| it.url())
.or_else(|| self.items().next())
.unwrap_or_default()
}
pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
self.0.children().find_map(MatchingPattern::cast)
}
pub fn matching_pattern(&self) -> Option<String> {
self.matching_pattern_node()
.map(|it| it.pattern())
.or_else(|| {
self.items().nth(1)
})
}
pub fn version_node(&self) -> Option<VersionPolicyNode> {
self.0.children().find_map(VersionPolicyNode::cast)
}
pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
self.version_node()
.map(|it| it.policy().parse())
.transpose()
.map_err(|e: crate::types::ParseError| e.to_string())
.or_else(|_e| {
self.items()
.nth(2)
.map(|it| it.parse())
.transpose()
.map_err(|e: crate::types::ParseError| e.to_string())
})
}
pub fn script_node(&self) -> Option<ScriptNode> {
self.0.children().find_map(ScriptNode::cast)
}
pub fn script(&self) -> Option<String> {
self.script_node().map(|it| it.script()).or_else(|| {
self.items().nth(3)
})
}
pub fn format_url(
&self,
package: impl FnOnce() -> String,
component: impl FnOnce() -> String,
) -> url::Url {
crate::subst::subst(self.url().as_str(), package, component)
.parse()
.unwrap()
}
pub fn set_url(&mut self, new_url: &str) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(URL.into());
builder.token(VALUE.into(), new_url);
builder.finish_node();
let new_url_green = builder.finish();
let new_url_node = SyntaxNode::new_root_mut(new_url_green);
let url_pos = self
.0
.children_with_tokens()
.position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
if let Some(pos) = url_pos {
self.0
.splice_children(pos..pos + 1, vec![new_url_node.into()]);
}
}
pub fn set_matching_pattern(&mut self, new_pattern: &str) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(MATCHING_PATTERN.into());
builder.token(VALUE.into(), new_pattern);
builder.finish_node();
let new_pattern_green = builder.finish();
let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
let pattern_pos = self.0.children_with_tokens().position(
|child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
);
if let Some(pos) = pattern_pos {
self.0
.splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
}
}
pub fn set_version_policy(&mut self, new_policy: &str) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(VERSION_POLICY.into());
builder.token(VALUE.into(), new_policy);
builder.finish_node();
let new_policy_green = builder.finish();
let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
let policy_pos = self.0.children_with_tokens().position(
|child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
);
if let Some(pos) = policy_pos {
self.0
.splice_children(pos..pos + 1, vec![new_policy_node.into()]);
}
}
pub fn set_script(&mut self, new_script: &str) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(SCRIPT.into());
builder.token(VALUE.into(), new_script);
builder.finish_node();
let new_script_green = builder.finish();
let new_script_node = SyntaxNode::new_root_mut(new_script_green);
let script_pos = self
.0
.children_with_tokens()
.position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
if let Some(pos) = script_pos {
self.0
.splice_children(pos..pos + 1, vec![new_script_node.into()]);
}
}
pub fn set_option(&mut self, option: crate::types::WatchOption) {
let key = watch_option_to_key(&option);
let value = watch_option_to_value(&option);
self.set_opt(key, &value);
}
pub fn set_opt(&mut self, key: &str, value: &str) {
let opts_pos = self.0.children_with_tokens().position(
|child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
);
if let Some(_opts_idx) = opts_pos {
if let Some(mut ol) = self.option_list() {
if let Some(mut opt) = ol.find_option(key) {
opt.set_value(value);
} else {
ol.add_option(key, value);
}
}
} else {
let mut builder = GreenNodeBuilder::new();
builder.start_node(OPTS_LIST.into());
builder.token(KEY.into(), "opts");
builder.token(EQUALS.into(), "=");
builder.start_node(OPTION.into());
builder.token(KEY.into(), key);
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), value);
builder.finish_node();
builder.finish_node();
let new_opts_green = builder.finish();
let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
let url_pos = self
.0
.children_with_tokens()
.position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
if let Some(url_idx) = url_pos {
let mut combined_builder = GreenNodeBuilder::new();
combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
combined_builder.finish_node();
let temp_green = combined_builder.finish();
let temp_root = SyntaxNode::new_root_mut(temp_green);
let space_element = temp_root.children_with_tokens().next().unwrap();
self.0
.splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
} else {
self.0.splice_children(0..0, vec![new_opts_node.into()]);
}
}
}
pub fn del_opt(&mut self, option: crate::types::WatchOption) {
let key = watch_option_to_key(&option);
if let Some(mut ol) = self.option_list() {
let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
if option_count == 1 && ol.has_option(key) {
let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
if let Some(opts_idx) = opts_pos {
self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
while self.0.children_with_tokens().next().is_some_and(|e| {
matches!(
e,
SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
)
}) {
self.0.splice_children(0..1, vec![]);
}
}
} else {
ol.remove_option(key);
}
}
}
pub fn del_opt_str(&mut self, key: &str) {
if let Some(mut ol) = self.option_list() {
let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
if option_count == 1 && ol.has_option(key) {
let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
if let Some(opts_idx) = opts_pos {
self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
while self.0.children_with_tokens().next().is_some_and(|e| {
matches!(
e,
SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
)
}) {
self.0.splice_children(0..1, vec![]);
}
}
} else {
ol.remove_option(key);
}
}
}
}
impl std::fmt::Debug for OptionList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OptionList")
.field("text", &self.0.text().to_string())
.finish()
}
}
impl OptionList {
pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
self.0.children().filter_map(_Option::cast)
}
pub fn find_option(&self, key: &str) -> Option<_Option> {
self.options().find(|opt| opt.key().as_deref() == Some(key))
}
pub fn has_option(&self, key: &str) -> bool {
self.options().any(|it| it.key().as_deref() == Some(key))
}
#[cfg(feature = "deb822")]
pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
self.options().filter_map(|opt| {
if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
Some((key, value))
} else {
None
}
})
}
pub fn get_option(&self, key: &str) -> Option<String> {
for child in self.options() {
if child.key().as_deref() == Some(key) {
return child.value();
}
}
None
}
fn add_option(&mut self, key: &str, value: &str) {
let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
let mut builder = GreenNodeBuilder::new();
builder.start_node(ROOT.into());
if option_count > 0 {
builder.start_node(OPTION_SEPARATOR.into());
builder.token(COMMA.into(), ",");
builder.finish_node();
}
builder.start_node(OPTION.into());
builder.token(KEY.into(), key);
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), value);
builder.finish_node();
builder.finish_node(); let combined_green = builder.finish();
let temp_root = SyntaxNode::new_root_mut(combined_green);
let new_children: Vec<_> = temp_root.children_with_tokens().collect();
let insert_pos = self.0.children_with_tokens().count();
self.0.splice_children(insert_pos..insert_pos, new_children);
}
fn remove_option(&mut self, key: &str) -> bool {
if let Some(mut opt) = self.find_option(key) {
opt.remove();
true
} else {
false
}
}
}
impl _Option {
pub fn key(&self) -> Option<String> {
self.0.children_with_tokens().find_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
_ => None,
})
}
pub fn value(&self) -> Option<String> {
self.0
.children_with_tokens()
.filter_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE || token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
_ => None,
})
.nth(1)
}
pub fn set_value(&mut self, new_value: &str) {
let key = self.key().expect("Option must have a key");
let mut builder = GreenNodeBuilder::new();
builder.start_node(OPTION.into());
builder.token(KEY.into(), &key);
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), new_value);
builder.finish_node();
let new_option_green = builder.finish();
let new_option_node = SyntaxNode::new_root_mut(new_option_green);
if let Some(parent) = self.0.parent() {
let idx = self.0.index();
parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
}
}
pub fn remove(&mut self) {
let next_sep = self
.0
.next_sibling()
.filter(|n| n.kind() == OPTION_SEPARATOR);
let prev_sep = self
.0
.prev_sibling()
.filter(|n| n.kind() == OPTION_SEPARATOR);
if let Some(sep) = next_sep {
sep.detach();
} else if let Some(sep) = prev_sep {
sep.detach();
}
self.0.detach();
}
}
fn join_tokens(node: &SyntaxNode, keep: impl Fn(SyntaxKind) -> bool) -> String {
let mut out = String::new();
for it in node.children_with_tokens() {
if let SyntaxElement::Token(token) = it {
if keep(token.kind()) {
out.push_str(token.text());
}
}
}
out
}
impl Url {
pub fn url(&self) -> String {
join_tokens(&self.0, |k| {
matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
})
}
}
impl MatchingPattern {
pub fn pattern(&self) -> String {
join_tokens(&self.0, |k| {
matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
})
}
}
impl VersionPolicyNode {
pub fn policy(&self) -> String {
join_tokens(&self.0, |k| {
matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
})
}
}
impl ScriptNode {
pub fn script(&self) -> String {
join_tokens(&self.0, |k| {
matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_node_structure() {
let wf: super::WatchFile = r#"version=4
opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(
entry
.0
.children()
.find(|n| n.kind() == MATCHING_PATTERN)
.is_some(),
true
);
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(
entry
.0
.children()
.find(|n| n.kind() == VERSION_POLICY)
.is_some(),
true
);
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(
entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
true
);
assert_eq!(entry.script(), Some("uupdate".into()));
}
#[test]
fn test_entry_node_structure_partial() {
let wf: super::WatchFile = r#"version=4
https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
assert_eq!(
entry
.0
.children()
.find(|n| n.kind() == MATCHING_PATTERN)
.is_some(),
true
);
assert_eq!(
entry
.0
.children()
.find(|n| n.kind() == VERSION_POLICY)
.is_some(),
false
);
assert_eq!(
entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
false
);
assert_eq!(entry.url(), "https://github.com/example/tags");
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(None));
assert_eq!(entry.script(), None);
}
#[test]
fn test_parse_v1() {
const WATCHV1: &str = r#"version=4
opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#;
let parsed = parse(WATCHV1);
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r#"ROOT@0..161
VERSION@0..10
KEY@0..7 "version"
EQUALS@7..8 "="
VALUE@8..9 "4"
NEWLINE@9..10 "\n"
ENTRY@10..161
OPTS_LIST@10..86
KEY@10..14 "opts"
EQUALS@14..15 "="
OPTION@15..19
KEY@15..19 "bare"
OPTION_SEPARATOR@19..20
COMMA@19..20 ","
OPTION@20..86
KEY@20..34 "filenamemangle"
EQUALS@34..35 "="
VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
WHITESPACE@86..87 " "
CONTINUATION@87..89 "\\\n"
WHITESPACE@89..91 " "
URL@91..138
VALUE@91..138 "https://github.com/sy ..."
WHITESPACE@138..139 " "
MATCHING_PATTERN@139..160
VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
NEWLINE@160..161 "\n"
"#
);
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(None));
assert_eq!(entry.script(), None);
assert_eq!(node.text(), WATCHV1);
}
#[test]
fn test_parse_v2() {
let parsed = parse(
r#"version=4
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
# comment
"#,
);
assert_eq!(parsed.errors, Vec::<String>::new());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r###"ROOT@0..90
VERSION@0..10
KEY@0..7 "version"
EQUALS@7..8 "="
VALUE@8..9 "4"
NEWLINE@9..10 "\n"
ENTRY@10..80
URL@10..57
VALUE@10..57 "https://github.com/sy ..."
WHITESPACE@57..58 " "
MATCHING_PATTERN@58..79
VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
NEWLINE@79..80 "\n"
COMMENT@80..89 "# comment"
NEWLINE@89..90 "\n"
"###
);
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
assert_eq!(
entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
"https://github.com/syncthing/syncthing-gtk/tags"
.parse()
.unwrap()
);
}
#[test]
fn test_parse_v3() {
let parsed = parse(
r#"version=4
https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
# comment
"#,
);
assert_eq!(parsed.errors, Vec::<String>::new());
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
assert_eq!(
entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
"https://github.com/syncthing/syncthing-gtk/tags"
.parse()
.unwrap()
);
}
#[test]
fn test_thread_safe_parsing() {
let text = r#"version=4
https://github.com/example/example/tags example-(.*)\.tar\.gz
"#;
let parsed = parse_watch_file(text);
assert!(parsed.is_ok());
assert_eq!(parsed.errors().len(), 0);
let watchfile = parsed.tree();
assert_eq!(watchfile.version(), 4);
let entries: Vec<_> = watchfile.entries().collect();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_parse_clone_and_eq() {
let text = r#"version=4
https://github.com/example/example/tags example-(.*)\.tar\.gz
"#;
let parsed1 = parse_watch_file(text);
let parsed2 = parsed1.clone();
assert_eq!(parsed1, parsed2);
let watchfile1 = parsed1.tree();
let watchfile2 = watchfile1.clone();
assert_eq!(watchfile1, watchfile2);
}
#[test]
fn test_parse_v4() {
let cl: super::WatchFile = r#"version=4
opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
https://github.com/example/example-cat/tags \
(?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
assert_eq!(cl.version(), 4);
let entries = cl.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert!(entry.repack());
assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
assert_eq!(entry.repacksuffix(), Some("+ds".into()));
assert_eq!(entry.script(), Some("uupdate".into()));
assert_eq!(
entry.format_url(|| "example-cat".to_string(), || String::new()),
"https://github.com/example/example-cat/tags"
.parse()
.unwrap()
);
assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
}
#[test]
fn test_git_mode() {
let text = r#"version=3
opts="mode=git, gitmode=shallow, pgpmode=gittag" \
https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
refs/tags/(.*) debian
"#;
let parsed = parse(text);
assert_eq!(parsed.errors, Vec::<String>::new());
let cl = parsed.root();
assert_eq!(cl.version(), 3);
let entries = cl.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
);
assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
assert_eq!(entry.script(), None);
assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
assert_eq!(entry.mode(), Ok(Mode::Git));
}
#[test]
fn test_parse_quoted() {
const WATCHV1: &str = r#"version=4
opts="bare, filenamemangle=blah" \
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#;
let parsed = parse(WATCHV1);
let node = parsed.syntax();
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(None));
assert_eq!(entry.script(), None);
assert_eq!(node.text(), WATCHV1);
}
#[test]
fn test_set_url() {
let wf: super::WatchFile = r#"version=4
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
entry.set_url("https://newurl.example.org/path");
assert_eq!(entry.url(), "https://newurl.example.org/path");
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(
entry.to_string(),
"https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_set_url_with_options() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.url(), "https://foo.com/bar");
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
entry.set_url("https://example.com/baz");
assert_eq!(entry.url(), "https://example.com/baz");
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(
entry.to_string(),
"opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_set_url_complex() {
let wf: super::WatchFile = r#"version=4
opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
entry.set_url("https://gitlab.com/newproject/tags");
assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
assert!(entry.bare());
assert_eq!(
entry.filenamemangle(),
Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
);
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(
entry.to_string(),
r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
"#
);
}
#[test]
fn test_set_url_with_all_fields() {
let wf: super::WatchFile = r#"version=4
opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
https://github.com/example/example-cat/tags \
(?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(entry.script(), Some("uupdate".into()));
entry.set_url("https://gitlab.example.org/project/releases");
assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
assert!(entry.repack());
assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
assert_eq!(entry.repacksuffix(), Some("+ds".into()));
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(entry.script(), Some("uupdate".into()));
assert_eq!(
entry.to_string(),
r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
https://gitlab.example.org/project/releases \
(?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
);
}
#[test]
fn test_set_url_quoted_options() {
let wf: super::WatchFile = r#"version=4
opts="bare, filenamemangle=blah" \
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
entry.set_url("https://example.org/new/path");
assert_eq!(entry.url(), "https://example.org/new/path");
assert_eq!(
entry.to_string(),
r#"opts="bare, filenamemangle=blah" \
https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
"#
);
}
#[test]
fn test_set_opt_update_existing() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
entry.set_opt("foo", "updated");
assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
assert_eq!(
entry.to_string(),
"opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_set_opt_add_new() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), None);
entry.set_opt("bar", "baz");
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
assert_eq!(
entry.to_string(),
"opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_set_opt_create_options_list() {
let wf: super::WatchFile = r#"version=4
https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.option_list(), None);
entry.set_opt("compression", "xz");
assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
assert_eq!(
entry.to_string(),
"opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_del_opt_remove_single() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
entry.del_opt_str("bar");
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), None);
assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
assert_eq!(
entry.to_string(),
"opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_del_opt_remove_first() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
entry.del_opt_str("foo");
assert_eq!(entry.get_option("foo"), None);
assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
assert_eq!(
entry.to_string(),
"opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_del_opt_remove_last() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
entry.del_opt_str("bar");
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
assert_eq!(entry.get_option("bar"), None);
assert_eq!(
entry.to_string(),
"opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_del_opt_remove_only_option() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
entry.del_opt_str("foo");
assert_eq!(entry.get_option("foo"), None);
assert_eq!(entry.option_list(), None);
assert_eq!(
entry.to_string(),
"https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
);
}
#[test]
fn test_del_opt_nonexistent() {
let wf: super::WatchFile = r#"version=4
opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
let original = entry.to_string();
entry.del_opt_str("nonexistent");
assert_eq!(entry.to_string(), original);
}
#[test]
fn test_set_opt_multiple_operations() {
let wf: super::WatchFile = r#"version=4
https://example.com/releases .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
entry.set_opt("compression", "xz");
entry.set_opt("repack", "");
entry.set_opt("dversionmangle", "s/\\+ds//");
assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
assert_eq!(
entry.get_option("dversionmangle"),
Some("s/\\+ds//".to_string())
);
}
#[test]
fn test_set_matching_pattern() {
let wf: super::WatchFile = r#"version=4
https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
);
assert_eq!(entry.url(), "https://github.com/example/tags");
assert_eq!(
entry.to_string(),
"https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
);
}
#[test]
fn test_set_matching_pattern_with_all_fields() {
let wf: super::WatchFile = r#"version=4
opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
assert_eq!(
entry.matching_pattern(),
Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
);
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(entry.script(), Some("uupdate".into()));
assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
assert_eq!(
entry.to_string(),
"opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
);
}
#[test]
fn test_set_version_policy() {
let wf: super::WatchFile = r#"version=4
https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
entry.set_version_policy("previous");
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.script(), Some("uupdate".into()));
assert_eq!(
entry.to_string(),
"https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
);
}
#[test]
fn test_set_version_policy_with_options() {
let wf: super::WatchFile = r#"version=4
opts=repack,compression=xz \
https://github.com/example/example-cat/tags \
(?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
entry.set_version_policy("ignore");
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.script(), Some("uupdate".into()));
assert!(entry.repack());
assert_eq!(
entry.to_string(),
r#"opts=repack,compression=xz \
https://github.com/example/example-cat/tags \
(?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
"#
);
}
#[test]
fn test_set_script() {
let wf: super::WatchFile = r#"version=4
https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.script(), Some("uupdate".into()));
entry.set_script("uscan");
assert_eq!(entry.script(), Some("uscan".into()));
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(
entry.to_string(),
"https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
);
}
#[test]
fn test_set_script_with_options() {
let wf: super::WatchFile = r#"version=4
opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#
.parse()
.unwrap();
let mut entry = wf.entries().next().unwrap();
assert_eq!(entry.script(), Some("uupdate".into()));
entry.set_script("custom-script.sh");
assert_eq!(entry.script(), Some("custom-script.sh".into()));
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(
entry.matching_pattern(),
Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
assert_eq!(
entry.to_string(),
"opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
);
}
#[test]
fn test_apply_dversionmangle() {
let wf: super::WatchFile = r#"version=4
opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
let wf: super::WatchFile = r#"version=4
opts=versionmangle=s/^v// https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
let wf: super::WatchFile = r#"version=4
opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
}
#[test]
fn test_apply_oversionmangle() {
let wf: super::WatchFile = r#"version=4
opts=oversionmangle=s/$/-1/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
let wf: super::WatchFile = r#"version=4
opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
}
#[test]
fn test_apply_dirversionmangle() {
let wf: super::WatchFile = r#"version=4
opts=dirversionmangle=s/^v// https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
let wf: super::WatchFile = r#"version=4
opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
}
#[test]
fn test_apply_filenamemangle() {
let wf: super::WatchFile = r#"version=4
opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_filenamemangle("https://example.com/v1.0.tar.gz")
.unwrap(),
"mypackage-1.0.tar.gz"
);
assert_eq!(
entry
.apply_filenamemangle("https://example.com/2.5.3.tar.gz")
.unwrap(),
"mypackage-2.5.3.tar.gz"
);
let wf: super::WatchFile = r#"version=4
opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_filenamemangle("https://example.com/path/to/file.tar.gz")
.unwrap(),
"file.tar.gz"
);
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_filenamemangle("https://example.com/file.tar.gz")
.unwrap(),
"https://example.com/file.tar.gz"
);
}
#[test]
fn test_apply_pagemangle() {
let wf: super::WatchFile = r#"version=4
opts=pagemangle=s/&/&/g https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry.apply_pagemangle(b"foo & bar").unwrap(),
b"foo & bar"
);
assert_eq!(
entry
.apply_pagemangle(b"& foo & bar &")
.unwrap(),
b"& foo & bar &"
);
let wf: super::WatchFile = r#"version=4
opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry.apply_pagemangle(b"foo & bar").unwrap(),
b"foo & bar"
);
}
#[test]
fn test_apply_downloadurlmangle() {
let wf: super::WatchFile = r#"version=4
opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
.unwrap(),
"https://example.com/download/file.tar.gz"
);
let wf: super::WatchFile = r#"version=4
opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
.unwrap(),
"https://raw.githubusercontent.com/user/repo/file.tar.gz"
);
let wf: super::WatchFile = r#"version=4
https://example.com/ .*
"#
.parse()
.unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(
entry
.apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
.unwrap(),
"https://example.com/archive/file.tar.gz"
);
}
#[test]
fn test_entry_builder_minimal() {
let entry = super::EntryBuilder::new("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.build();
assert_eq!(entry.url(), "https://github.com/example/tags");
assert_eq!(
entry.matching_pattern().as_deref(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz")
);
assert_eq!(entry.version(), Ok(None));
assert_eq!(entry.script(), None);
assert!(entry.opts().is_empty());
}
#[test]
fn test_entry_builder_url_only() {
let entry = super::EntryBuilder::new("https://example.com/releases").build();
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(entry.matching_pattern(), None);
assert_eq!(entry.version(), Ok(None));
assert_eq!(entry.script(), None);
assert!(entry.opts().is_empty());
}
#[test]
fn test_entry_builder_with_all_fields() {
let entry = super::EntryBuilder::new("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
.version_policy("debian")
.script("uupdate")
.opt("compression", "xz")
.flag("repack")
.build();
assert_eq!(entry.url(), "https://github.com/example/tags");
assert_eq!(
entry.matching_pattern().as_deref(),
Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
);
assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
assert_eq!(entry.script(), Some("uupdate".into()));
assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
assert!(entry.has_option("repack"));
assert!(entry.repack());
}
#[test]
fn test_entry_builder_multiple_options() {
let entry = super::EntryBuilder::new("https://example.com/tags")
.matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
.opt("compression", "xz")
.opt("dversionmangle", "s/\\+ds//")
.opt("repacksuffix", "+ds")
.build();
assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
assert_eq!(
entry.get_option("dversionmangle"),
Some("s/\\+ds//".to_string())
);
assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
}
#[test]
fn test_entry_builder_via_entry() {
let entry = super::Entry::builder("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.version_policy("debian")
.build();
assert_eq!(entry.url(), "https://github.com/example/tags");
assert_eq!(
entry.matching_pattern().as_deref(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz")
);
assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
}
#[test]
fn test_watchfile_add_entry_to_empty() {
let mut wf = super::WatchFile::new(Some(4));
let entry = super::EntryBuilder::new("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.build();
wf.add_entry(entry);
assert_eq!(wf.version(), 4);
assert_eq!(wf.entries().count(), 1);
let added_entry = wf.entries().next().unwrap();
assert_eq!(added_entry.url(), "https://github.com/example/tags");
assert_eq!(
added_entry.matching_pattern().as_deref(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz")
);
}
#[test]
fn test_watchfile_add_multiple_entries() {
let mut wf = super::WatchFile::new(Some(4));
wf.add_entry(
super::EntryBuilder::new("https://github.com/example1/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.build(),
);
wf.add_entry(
super::EntryBuilder::new("https://github.com/example2/releases")
.matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
.opt("compression", "xz")
.build(),
);
assert_eq!(wf.entries().count(), 2);
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries[0].url(), "https://github.com/example1/tags");
assert_eq!(entries[1].url(), "https://github.com/example2/releases");
assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
}
#[test]
fn test_watchfile_add_entry_to_existing() {
let mut wf: super::WatchFile = r#"version=4
https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
"#
.parse()
.unwrap();
assert_eq!(wf.entries().count(), 1);
wf.add_entry(
super::EntryBuilder::new("https://github.com/example/new")
.matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
.opt("compression", "xz")
.version_policy("debian")
.build(),
);
assert_eq!(wf.entries().count(), 2);
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries[0].url(), "https://example.com/old");
assert_eq!(entries[1].url(), "https://github.com/example/new");
assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
}
#[test]
fn test_entry_builder_formatting() {
let entry = super::EntryBuilder::new("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.opt("compression", "xz")
.flag("repack")
.version_policy("debian")
.script("uupdate")
.build();
let entry_str = entry.to_string();
assert!(entry_str.starts_with("opts="));
assert!(entry_str.contains("https://github.com/example/tags"));
assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
assert!(entry_str.contains("debian"));
assert!(entry_str.contains("uupdate"));
assert!(entry_str.ends_with('\n'));
}
#[test]
fn test_watchfile_add_entry_preserves_format() {
let mut wf = super::WatchFile::new(Some(4));
wf.add_entry(
super::EntryBuilder::new("https://github.com/example/tags")
.matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
.build(),
);
let wf_str = wf.to_string();
assert!(wf_str.starts_with("version=4\n"));
assert!(wf_str.contains("https://github.com/example/tags"));
let reparsed: super::WatchFile = wf_str.parse().unwrap();
assert_eq!(reparsed.version(), 4);
assert_eq!(reparsed.entries().count(), 1);
}
#[test]
fn test_line_col() {
let text = r#"version=4
opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
"#;
let wf = text.parse::<super::WatchFile>().unwrap();
let version_node = wf.version_node().unwrap();
assert_eq!(version_node.line(), 0);
assert_eq!(version_node.column(), 0);
assert_eq!(version_node.line_col(), (0, 0));
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].line(), 1);
assert_eq!(entries[0].column(), 0);
assert_eq!(entries[0].line_col(), (1, 0));
let option_list = entries[0].option_list().unwrap();
assert_eq!(option_list.line(), 1);
let url_node = entries[0].url_node().unwrap();
assert_eq!(url_node.line(), 1);
let pattern_node = entries[0].matching_pattern_node().unwrap();
assert_eq!(pattern_node.line(), 1);
let version_policy_node = entries[0].version_node().unwrap();
assert_eq!(version_policy_node.line(), 1);
let script_node = entries[0].script_node().unwrap();
assert_eq!(script_node.line(), 1);
let options: Vec<_> = option_list.options().collect();
assert_eq!(options.len(), 1);
assert_eq!(options[0].key(), Some("compression".to_string()));
assert_eq!(options[0].value(), Some("xz".to_string()));
assert_eq!(options[0].line(), 1);
let compression_opt = option_list.find_option("compression").unwrap();
assert_eq!(compression_opt.line(), 1);
assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
}
#[test]
fn test_parse_str_relaxed() {
let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
r#"version=4
ERRORS IN THIS LINE
opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
"#,
);
assert_eq!(wf.version(), 4);
assert_eq!(wf.entries().count(), 2);
let entries = wf.entries().collect::<Vec<_>>();
let entry = &entries[0];
assert_eq!(entry.url(), "ERRORS");
let entry = &entries[1];
assert_eq!(entry.url(), "https://example.com/releases");
assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
}
#[test]
fn test_parse_entry_with_comment_before() {
let input = concat!(
"version=4\n",
"# try also https://pypi.debian.net/tomoscan/watch\n",
"opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
"https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
);
let wf: super::WatchFile = input.parse().unwrap();
assert_eq!(wf.to_string(), input);
assert_eq!(wf.entries().count(), 1);
let entry = wf.entries().next().unwrap();
assert_eq!(
entry.url(),
"https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
);
assert_eq!(
entry.get_option("uversionmangle"),
Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
);
}
#[test]
fn test_parse_multiple_comments_before_entry() {
let input = concat!(
"version=4\n",
"# first comment\n",
"# second comment\n",
"# third comment\n",
"https://example.com/foo foo-(.*).tar.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
assert_eq!(wf.to_string(), input);
assert_eq!(wf.entries().count(), 1);
assert_eq!(
wf.entries().next().unwrap().url(),
"https://example.com/foo"
);
}
#[test]
fn test_parse_blank_lines_between_entries() {
let input = concat!(
"version=4\n",
"https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
"\n",
"https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
assert_eq!(wf.to_string(), input);
assert_eq!(wf.entries().count(), 2);
}
#[test]
fn test_parse_trailing_unparseable_tokens_produce_error() {
let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
let result = input.parse::<super::WatchFile>();
assert!(result.is_err(), "expected parse error for trailing garbage");
let wf = super::WatchFile::from_str_relaxed(input);
assert_eq!(wf.to_string(), input);
}
#[test]
fn test_parse_roundtrip_full_file() {
let inputs = [
"version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
"version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
concat!(
"version=4\n",
"opts=uversionmangle=s/rc/~rc/ \\\n",
" https://example.com/foo foo-(.*).tar.gz\n",
),
concat!(
"version=4\n",
"# comment before entry\n",
"opts=uversionmangle=s/rc/~rc/ \\\n",
"https://example.com/foo foo-(.*).tar.gz\n",
"# comment between entries\n",
"https://example.com/bar bar-(.*).tar.gz\n",
),
];
for input in &inputs {
let wf: super::WatchFile = input.parse().unwrap();
assert_eq!(
wf.to_string(),
*input,
"round-trip failed for input: {:?}",
input
);
}
}
#[test]
fn test_parse_url_with_equals_in_query_string() {
let input = concat!(
"version=4\n",
"https://api.github.com/repos/x/releases?per_page=100 \\\n",
" https://github.com/x/v[^/]+/x.tar.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].url(),
"https://api.github.com/repos/x/releases?per_page=100"
);
assert_eq!(
entries[0].matching_pattern().as_deref(),
Some("https://github.com/x/v[^/]+/x.tar.gz"),
);
assert_eq!(wf.to_string(), input);
}
#[test]
fn test_entry_url_does_not_panic_when_empty() {
let input = "version=4\n=garbage\n";
let wf = super::WatchFile::from_str_relaxed(input);
for entry in wf.entries() {
let _ = entry.url();
}
}
#[test]
fn test_parse_url_node_with_equals_join_tokens() {
let input = "version=4\nhttps://example.com/x?y=1&z=2 .*tar.gz\n";
let wf: super::WatchFile = input.parse().unwrap();
let entry = wf.entries().next().unwrap();
assert_eq!(entry.url(), "https://example.com/x?y=1&z=2");
}
#[test]
fn test_parse_quoted_opts_with_trailing_comma_continuation() {
let input = concat!(
"version=4\n\n",
"opts=\"\\\n",
"pgpmode=none,\\\n",
"repack,compression=xz,repacksuffix=+dfsg,\\\n",
"dversionmangle=s{[+~]dfsg\\d*}{},\\\n",
"\" https://github.com/varlink/go/releases \\\n",
" .*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].url(), "https://github.com/varlink/go/releases");
assert_eq!(
entries[0].matching_pattern().as_deref(),
Some(".*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz"),
);
assert_eq!(wf.to_string(), input);
}
#[test]
fn test_parse_quoted_opts_with_spaces_around_comma() {
let input = concat!(
"version=4\n",
"opts=\"filenamemangle=s/.+\\/v?(\\d\\S*)\\.tar\\.gz/v$1.tar.gz/ , uversionmangle=tr%-rc%~rc%\" \\\n",
" https://github.com/analogdevicesinc/libiio/tags .*/v(\\d\\S*)\\.tar\\.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].url(),
"https://github.com/analogdevicesinc/libiio/tags",
);
assert_eq!(wf.to_string(), input);
}
#[test]
fn test_parse_unquoted_opts_trailing_comma_then_url() {
let input = concat!(
"version=3\n",
"opts=uversionmangle=s/(rc|a|b|c)/~$1/,\\\n",
"https://github.com/openstack/rally/tags .*/(\\d\\S*)\\.tar\\.gz\n",
);
let wf: super::WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].url(), "https://github.com/openstack/rally/tags");
assert_eq!(
entries[0].matching_pattern().as_deref(),
Some(".*/(\\d\\S*)\\.tar\\.gz"),
);
assert_eq!(wf.to_string(), input);
}
#[test]
fn test_parse_unquoted_opts_value_with_equals() {
let input = concat!(
"version=4\n",
"opts=dversionmangle=s/\\~dfsg//,downloadurlmangle=s/.*ref=//,pgpsigurlmangle=s/$/.asc/ \\\n",
"\thttps://downloads.asterisk.org/pub/telephony/libpri/releases/ libpri-([0-9.]*)\\.tar\\.gz debian uupdate\n",
);
let wf: super::WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].url(),
"https://downloads.asterisk.org/pub/telephony/libpri/releases/"
);
assert_eq!(
entries[0].matching_pattern().as_deref(),
Some("libpri-([0-9.]*)\\.tar\\.gz"),
);
assert_eq!(wf.to_string(), input);
}
}