Browse Source

initial commit

master
boB Rudis 4 years ago
commit
d607d2c6be
No known key found for this signature in database GPG Key ID: 1D7529BE14E2BBA9
  1. 11
      .Rbuildignore
  2. 1
      .codecov.yml
  3. 8
      .gitignore
  4. 6
      .travis.yml
  5. 36
      DESCRIPTION
  6. 42
      NAMESPACE
  7. 3
      NEWS.md
  8. 11
      R/RcppExports.R
  9. 6
      R/aaa.R
  10. 29
      R/apache-httpd.R
  11. 35
      R/lighttpd.R
  12. 44
      R/mongodb.R
  13. 34
      R/nginx.R
  14. 36
      R/sendmail.R
  15. 25
      R/vershist-package.R
  16. 77
      README.Rmd
  17. 153
      README.md
  18. 15
      man/apache_httpd_version_history.Rd
  19. 14
      man/is_valid.Rd
  20. 15
      man/lighttpd_version_history.Rd
  21. 19
      man/mongodb_version_history.Rd
  22. 15
      man/nginx_version_history.Rd
  23. 15
      man/sendmail_version_history.Rd
  24. 14
      man/vershist.Rd
  25. 3
      src/.gitignore
  26. 2
      src/Makevars
  27. 28
      src/RcppExports.cpp
  28. 218
      src/base/type.h
  29. 69
      src/base/util.h
  30. 534
      src/cpp-semver.h
  31. 230
      src/parser/parser.h
  32. 33
      src/vershist-main.cpp
  33. 2
      tests/test-all.R
  34. 6
      tests/testthat/test-vershist.R
  35. 21
      vershist.Rproj

11
.Rbuildignore

@ -0,0 +1,11 @@
^.*\.Rproj$
^\.Rproj\.user$
^\.travis\.yml$
^README\.*Rmd$
^README\.*html$
^NOTES\.*Rmd$
^NOTES\.*html$
^\.codecov\.yml$
^README_files$
^doc$
^tmp$

1
.codecov.yml

@ -0,0 +1 @@
comment: false

8
.gitignore

@ -0,0 +1,8 @@
.DS_Store
.Rproj.user
.Rhistory
.RData
.Rproj
src/*.o
src/*.so
src/*.dll

6
.travis.yml

@ -0,0 +1,6 @@
language: R
sudo: false
cache: packages
after_success:
- Rscript -e 'covr::codecov()'

36
DESCRIPTION

@ -0,0 +1,36 @@
Package: vershist
Type: Package
Title: Collect Version Histories For Vendor Products
Version: 0.1.0
Date: 2018-03-20
Authors@R: c(
person("Bob", "Rudis", email = "bob@rud.is", role = c("aut", "cre"),
comment = c(ORCID = "0000-0001-5670-2640"))
)
Maintainer: Bob Rudis <bob@rud.is>
Description: Provides a set of functions to gather version histories of products
(mainly software products) from their sources (generally websites).
URL: https://github.com/hrbrmstr/vershist
BugReports: https://github.com/hrbrmstr/vershist/issues
SystemRequirements: C++11
Encoding: UTF-8
License: AGPL
Suggests:
testthat,
covr
Depends:
R (>= 3.2.0)
Imports:
purrr,
rvest,
readr,
dplyr,
stringi,
semver,
lubridate,
utils,
xml2,
curl,
Rcpp
RoxygenNote: 6.0.1.9000
LinkingTo: Rcpp

42
NAMESPACE

@ -0,0 +1,42 @@
# Generated by roxygen2: do not edit by hand
export(apache_httpd_version_history)
export(is_valid)
export(lighttpd_version_history)
export(mongodb_version_history)
export(nginx_version_history)
export(sendmail_version_history)
import(semver)
importFrom(Rcpp,sourceCpp)
importFrom(curl,curl)
importFrom(dplyr,arrange)
importFrom(dplyr,as_data_frame)
importFrom(dplyr,bind_cols)
importFrom(dplyr,left_join)
importFrom(dplyr,mutate)
importFrom(dplyr,rename)
importFrom(dplyr,select)
importFrom(lubridate,mdy)
importFrom(lubridate,mdy_hms)
importFrom(lubridate,year)
importFrom(purrr,"%>%")
importFrom(purrr,discard)
importFrom(purrr,keep)
importFrom(purrr,map)
importFrom(purrr,map_df)
importFrom(readr,read_lines)
importFrom(rvest,html_nodes)
importFrom(rvest,html_text)
importFrom(rvest,xml_nodes)
importFrom(stringi,stri_detect_fixed)
importFrom(stringi,stri_detect_regex)
importFrom(stringi,stri_extract_first_regex)
importFrom(stringi,stri_match_first_regex)
importFrom(stringi,stri_replace_all_regex)
importFrom(stringi,stri_replace_first_fixed)
importFrom(stringi,stri_sub)
importFrom(utils,globalVariables)
importFrom(xml2,read_html)
importFrom(xml2,read_xml)
importFrom(xml2,xml_attr)
useDynLib(vershist)

3
NEWS.md

@ -0,0 +1,3 @@
0.1.0
* Initial release
* Support for Apache httpd, nginx, sendmail, lighttpd and mongodb

11
R/RcppExports.R

@ -0,0 +1,11 @@
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
#' Test if semantic version strings are valid
#'
#' @param v character verctor of version strings
#' @export
is_valid <- function(v) {
.Call('_vershist_is_valid', PACKAGE = 'vershist', v)
}

6
R/aaa.R

@ -0,0 +1,6 @@
utils::globalVariables(
c(
".", "vers", "major", "minor", "patch", "product", "rls_date", "rls_year",
"V1", "V2", "V3", "ts"
)
)

29
R/apache-httpd.R

@ -0,0 +1,29 @@
#' Retrive Apache httpd Version Release History
#'
#' Reads <https://archive.apache.org/dist/httpd/> to build a data frame of
#' Apache `httpd` version release numbers and dates with semantic version
#' strings parsed and separate fields added. The data frame is also arranged in
#' order from lowest version to latest version and the `vers` column is an
#' ordered factor.
#'
#' @md
#' @export
apache_httpd_version_history <- function() {
readr::read_lines("https://archive.apache.org/dist/httpd/") %>%
purrr::keep(stri_detect_regex, 'apache_.*gz"') %>%
stri_replace_first_fixed("<img src=\"/icons/compressed.gif\" alt=\"[ ]\"> ", "") %>%
stri_match_first_regex('href="apache_([[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+)\\.tar\\.gz".*([[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2})') %>%
dplyr::as_data_frame() %>%
dplyr::select(-V1) %>%
dplyr::rename(vers = V2, rls_date = V3) %>%
dplyr::mutate(rls_date = as.Date(rls_date)) %>%
dplyr::mutate(rls_year = lubridate::year(rls_date)) %>%
dplyr::bind_cols(
semver::parse_version(.$vers) %>%
as_data_frame()
) %>%
dplyr::arrange(major, minor, patch) %>%
dplyr::mutate(vers = factor(vers, levels = vers))
}

35
R/lighttpd.R

@ -0,0 +1,35 @@
#' Retrive lighttpd Version Release History
#'
#' Reads from the `lighttpd` releases and snapshot downloads to build a
#' data frame of version release numbers and dates. The caller is responsible
#' for extracting out the version components due to the non-standard
#' semantic versioning used. The [is_valid()] function can be used to test the
#' validity of version strings.
#'
#' @md
#' @export
lighttpd_version_history <- function() {
c("https://download.lighttpd.net/lighttpd/releases-1.4.x/",
"https://download.lighttpd.net/lighttpd/snapshots-1.5/",
"https://download.lighttpd.net/lighttpd/snapshots-1.4.x/",
"https://download.lighttpd.net/lighttpd/snapshots-2.0.x/"
) %>%
purrr::map_df(~{
pg <- read_html(.x)
dplyr::data_frame(
vers = rvest::html_nodes(pg, xpath=".//tr/td[1]") %>%
rvest::html_text(),
ts = rvest::html_nodes(pg, xpath=".//tr/td[2]") %>%
rvest::html_text()
)
}) %>%
dplyr::filter(stri_detect_regex(vers, "\\.tar\\.gz$")) %>%
dplyr::mutate(vers = stri_replace_all_regex(vers, "(^lighttpd-|\\.tar\\.gz$)", "")) %>%
dplyr::mutate(
ts = lubridate::ymd_hms(ts),
year = lubridate::year(ts)
) %>%
dplyr::rename(rls_date = ts, rls_year = year)
}

44
R/mongodb.R

@ -0,0 +1,44 @@
#' Retrive MongoDB Version Release History
#'
#' Reads <https://www.mongodb.org/dl/linux"> to build a data frame of
#' MongoDB version release numbers and dates with semantic version
#' strings parsed and separate fields added. The data frame is also arranged in
#' order from lowest version to latest version and the `vers` column is an
#' ordered factor.
#'
#' @md
#' @note Release candidates are not included and release history is only
#' supported for linux releases.
#' @export
mongodb_version_history <- function() {
pg <- xml2::read_html("https://www.mongodb.org/dl/linux")
dplyr::data_frame(
vers = rvest::html_nodes(pg, xpath=".//tr/td[1]") %>%
rvest::html_text(),
ts = rvest::html_nodes(pg, xpath=".//tr/td[2]") %>%
rvest::html_text()
) %>%
dplyr::filter(
!stri_detect_regex(vers, "(ubuntu|rhel|suse|debian|amazon|debug|latest)")
) %>%
dplyr::mutate(
vers = stri_extract_first_regex(
vers,
"[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]"
),
ts = stri_sub(ts, 1, 10)
) %>%
dplyr::distinct(vers, .keep_all=TRUE) %>%
dplyr::filter(!is.na(vers)) %>%
dplyr::mutate(year = lubridate::year(ts)) %>%
dplyr::bind_cols(
semver::parse_version(.$vers) %>%
dplyr::as_data_frame()
) %>%
dplyr::arrange(major, minor, patch) %>%
dplyr::mutate(vers = factor(vers, levels=vers)) %>%
dplyr::rename(rls_date = ts, rls_year = year)
}

34
R/nginx.R

@ -0,0 +1,34 @@
#' Retrive nginx Version Release History
#'
#' Reads <https://raw.githubusercontent.com/nginx/nginx/master/docs/xml/nginx/changes.xml>
#' to build a data frame of nginx version release numbers and dates with semantic version
#' strings parsed and separate fields added. The data frame is also arranged in
#' order from lowest version to latest version and the `vers` column is an
#' ordered factor.
#'
#' @md
#' @export
nginx_version_history <- function() {
nginx_changes_url <-
"https://raw.githubusercontent.com/nginx/nginx/master/docs/xml/nginx/changes.xml"
doc <- suppressWarnings(xml2::read_xml(nginx_changes_url))
dplyr::data_frame(
vers = rvest::xml_nodes(doc, xpath="//changes") %>%
xml2::xml_attr("ver"),
ts = rvest::xml_nodes(doc, xpath="//changes") %>%
xml2::xml_attr("date") %>%
as.Date(),
year = lubridate::year(ts)
) %>%
dplyr::bind_cols(
semver::parse_version(.$vers) %>%
dplyr::as_data_frame()
) %>%
dplyr::arrange(major, minor, patch) %>%
dplyr::mutate(vers = factor(vers, levels=vers)) %>%
dplyr::rename(rls_date = ts, rls_year = year)
}

36
R/sendmail.R

@ -0,0 +1,36 @@
#' Retrive sendmail Version Release History
#'
#' Reads <ftp://ftp.sendmail.org/pub/sendmail/"> to build a data frame of
#' `sendmail` version release numbers and dates with semantic version
#' strings parsed and separate fields added. The data frame is also arranged in
#' order from lowest version to latest version and the `vers` column is an
#' ordered factor.
#'
#' @md
#' @export
sendmail_version_history <- function() {
con <- curl::curl("ftp://ftp.sendmail.org/pub/sendmail/")
res <- readLines(con)
close(con)
stri_match_first_regex(res, "([[:alpha:]]{3} [[:digit:]]{2} [[:digit:]]{4}) (.*)") %>%
dplyr::as_data_frame() %>%
dplyr::select(-V1) %>%
dplyr::rename(vers = V3, ts = V2) %>%
dplyr::select(vers, ts) %>%
dplyr::filter(stri_detect_regex(vers, "^sendmail.*\\.tar\\.gz$")) %>%
dplyr::filter(!stri_detect_fixed(vers, "->")) %>%
dplyr::mutate(vers = stri_replace_all_regex(vers, "(^sendmail\\.|\\.tar\\.gz$)", "")) %>%
dplyr::mutate(ts = stri_replace_all_regex(ts, "\ +", " ")) %>%
dplyr::mutate(ts = lubridate::mdy(ts)) %>%
dplyr::mutate(year = lubridate::year(ts)) %>%
dplyr::rename(rls_date = ts, rls_year = year) %>%
dplyr::bind_cols(
semver::parse_version(.$vers) %>%
dplyr::as_data_frame()
) %>%
dplyr::arrange(major, minor, patch) %>%
dplyr::mutate(vers = factor(vers, levels=vers))
}

25
R/vershist-package.R

@ -0,0 +1,25 @@
#' Collect Version Histories For Vendor Products
#'
#' Provides a set of functions to gather version histories of products
#' (mainly software products) from their sources (generally websites).
#'
#' @md
#' @name vershist
#' @docType package
#' @author Bob Rudis (bob@@rud.is)
#' @import semver
#' @importFrom purrr keep discard map map_df %>%
#' @importFrom dplyr mutate rename select as_data_frame left_join bind_cols arrange
#' @importFrom dplyr rename
#' @importFrom stringi stri_match_first_regex stri_detect_fixed stri_detect_regex
#' @importFrom stringi stri_replace_all_regex stri_replace_first_fixed
#' @importFrom stringi stri_extract_first_regex stri_sub
#' @importFrom lubridate year mdy mdy_hms
#' @importFrom readr read_lines
#' @importFrom utils globalVariables
#' @importFrom xml2 read_html read_xml xml_attr
#' @importFrom rvest html_nodes html_text xml_nodes
#' @importFrom curl curl
#' @useDynLib vershist
#' @importFrom Rcpp sourceCpp
NULL

77
README.Rmd

@ -0,0 +1,77 @@
---
output: rmarkdown::github_document
---
# vershist
Collect Version Histories For Vendor Products
## Description
Provides a set of functions to gather version histories of products
(mainly software products) from their sources (generally websites).
## What's Inside The Tin
The following functions are implemented:
Core:
- `apache_httpd_version_history`: Retrive Apache httpd Version Release History
- `lighttpd_version_history`: Retrive lighttpd Version Release History
- `mongodb_version_history`: Retrive MongoDB Version Release History
- `nginx_version_history`: Retrive nginx Version Release History
- `sendmail_version_history`: Retrive sendmail Version Release History
Utility:
- `is_valid`: Test if semantic version strings are valid
## Installation
```{r eval=FALSE}
devtools::install_github("hrbrmstr/vershist")
```
```{r message=FALSE, warning=FALSE, error=FALSE, include=FALSE}
options(width=120)
```
## Usage
```{r message=FALSE, warning=FALSE, error=FALSE}
library(vershist)
# current verison
packageVersion("vershist")
```
Apache
```{r}
apache_httpd_version_history()
```
lighttpd
```{r}
lighttpd_version_history()
```
mongodb
```{r}
mongodb_version_history()
```
nginx
```{r}
nginx_version_history()
```
sendmail
```{r}
sendmail_version_history()
```

153
README.md

@ -0,0 +1,153 @@
# vershist
Collect Version Histories For Vendor Products
## Description
Provides a set of functions to gather version histories of products
(mainly software products) from their sources (generally websites).
## What’s Inside The Tin
The following functions are implemented:
Core:
- `apache_httpd_version_history`: Retrive Apache httpd Version Release
History
- `lighttpd_version_history`: Retrive lighttpd Version Release History
- `mongodb_version_history`: Retrive MongoDB Version Release History
- `nginx_version_history`: Retrive nginx Version Release History
- `sendmail_version_history`: Retrive sendmail Version Release History
Utility:
- `is_valid`: Test if semantic version strings are valid
## Installation
``` r
devtools::install_github("hrbrmstr/vershist")
```
## Usage
``` r
library(vershist)
# current verison
packageVersion("vershist")
```
## [1] '0.1.0'
Apache
``` r
apache_httpd_version_history()
```
## # A tibble: 29 x 8
## vers rls_date rls_year major minor patch prerelease build
## <fct> <date> <dbl> <int> <int> <int> <chr> <chr>
## 1 1.3.0 1998-06-05 1998 1 3 0 "" ""
## 2 1.3.1 1998-07-22 1998 1 3 1 "" ""
## 3 1.3.2 1998-09-21 1998 1 3 2 "" ""
## 4 1.3.3 1998-10-09 1998 1 3 3 "" ""
## 5 1.3.4 1999-01-10 1999 1 3 4 "" ""
## 6 1.3.6 1999-03-23 1999 1 3 6 "" ""
## 7 1.3.9 1999-08-19 1999 1 3 9 "" ""
## 8 1.3.11 2000-01-22 2000 1 3 11 "" ""
## 9 1.3.12 2000-02-25 2000 1 3 12 "" ""
## 10 1.3.14 2000-10-10 2000 1 3 14 "" ""
## # ... with 19 more rows
lighttpd
``` r
lighttpd_version_history()
```
## # A tibble: 97 x 3
## vers rls_date rls_year
## <chr> <dttm> <dbl>
## 1 1.4.20 2008-09-29 23:27:45 2008
## 2 1.4.21-r2389 2009-02-05 12:43:18 2009
## 3 1.4.13 2007-01-29 00:07:24 2007
## 4 1.4.41 2016-07-31 12:51:39 2016
## 5 1.4.27 2010-08-13 09:32:03 2010
## 6 1.4.46 2017-10-21 19:54:46 2017
## 7 1.4.39 2016-01-02 12:57:37 2016
## 8 1.4.40 2016-07-16 10:28:52 2016
## 9 1.4.32 2012-11-21 09:26:14 2012
## 10 1.4.30 2011-12-18 15:23:06 2011
## # ... with 87 more rows
mongodb
``` r
mongodb_version_history()
```
## # A tibble: 194 x 8
## vers rls_date rls_year major minor patch prerelease build
## <fct> <chr> <dbl> <int> <int> <int> <chr> <chr>
## 1 0.8.0 2009-02-11 2009 0 8 0 "" ""
## 2 0.9.0 2009-03-27 2009 0 9 0 "" ""
## 3 0.9.1 2009-08-24 2009 0 9 1 "" ""
## 4 0.9.2 2009-05-22 2009 0 9 2 "" ""
## 5 0.9.3 2009-05-29 2009 0 9 3 "" ""
## 6 0.9.4 2009-06-09 2009 0 9 4 "" ""
## 7 0.9.5 2009-06-23 2009 0 9 5 "" ""
## 8 0.9.6 2009-07-08 2009 0 9 6 "" ""
## 9 0.9.7 2009-07-29 2009 0 9 7 "" ""
## 10 0.9.8 2009-08-14 2009 0 9 8 "" ""
## # ... with 184 more rows
nginx
``` r
nginx_version_history()
```
## # A tibble: 423 x 8
## vers rls_date rls_year major minor patch prerelease build
## <fct> <date> <dbl> <int> <int> <int> <chr> <chr>
## 1 0.1.0 2004-10-04 2004 0 1 0 "" ""
## 2 0.1.1 2004-10-11 2004 0 1 1 "" ""
## 3 0.1.2 2004-10-21 2004 0 1 2 "" ""
## 4 0.1.3 2004-10-25 2004 0 1 3 "" ""
## 5 0.1.4 2004-10-26 2004 0 1 4 "" ""
## 6 0.1.5 2004-11-11 2004 0 1 5 "" ""
## 7 0.1.6 2004-11-11 2004 0 1 6 "" ""
## 8 0.1.7 2004-11-12 2004 0 1 7 "" ""
## 9 0.1.8 2004-11-20 2004 0 1 8 "" ""
## 10 0.1.9 2004-11-25 2004 0 1 9 "" ""
## # ... with 413 more rows
sendmail
``` r
sendmail_version_history()
```
## # A tibble: 16 x 8
## vers rls_date rls_year major minor patch prerelease build
## <fct> <date> <dbl> <int> <int> <int> <chr> <chr>
## 1 8.12.11 2004-01-18 2004 8 12 11 "" ""
## 2 8.13.6 2006-03-22 2006 8 13 6 "" ""
## 3 8.13.7 2006-06-05 2006 8 13 7 "" ""
## 4 8.13.8 2006-08-09 2006 8 13 8 "" ""
## 5 8.14.0 2007-02-01 2007 8 14 0 "" ""
## 6 8.14.1 2007-04-04 2007 8 14 1 "" ""
## 7 8.14.2 2007-11-01 2007 8 14 2 "" ""
## 8 8.14.3 2008-05-03 2008 8 14 3 "" ""
## 9 8.14.4 2009-12-29 2009 8 14 4 "" ""
## 10 8.14.5 2011-05-17 2011 8 14 5 "" ""
## 11 8.14.6 2012-12-23 2012 8 14 6 "" ""
## 12 8.14.7 2013-04-21 2013 8 14 7 "" ""
## 13 8.14.8 2014-01-26 2014 8 14 8 "" ""
## 14 8.14.9 2014-05-21 2014 8 14 9 "" ""
## 15 8.15.1 2014-12-06 2014 8 15 1 "" ""
## 16 8.15.2 2015-07-03 2015 8 15 2 "" ""

15
man/apache_httpd_version_history.Rd

@ -0,0 +1,15 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/apache-httpd.R
\name{apache_httpd_version_history}
\alias{apache_httpd_version_history}
\title{Retrive Apache httpd Version Release History}
\usage{
apache_httpd_version_history()
}
\description{
Reads \url{https://archive.apache.org/dist/httpd/} to build a data frame of
Apache \code{httpd} version release numbers and dates with semantic version
strings parsed and separate fields added. The data frame is also arranged in
order from lowest version to latest version and the \code{vers} column is an
ordered factor.
}

14
man/is_valid.Rd

@ -0,0 +1,14 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/RcppExports.R
\name{is_valid}
\alias{is_valid}
\title{Test if semantic version strings are valid}
\usage{
is_valid(v)
}
\arguments{
\item{v}{character verctor of version strings}
}
\description{
Test if semantic version strings are valid
}

15
man/lighttpd_version_history.Rd

@ -0,0 +1,15 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/lighttpd.R
\name{lighttpd_version_history}
\alias{lighttpd_version_history}
\title{Retrive lighttpd Version Release History}
\usage{
lighttpd_version_history()
}
\description{
Reads from the \code{lighttpd} releases and snapshot downloads to build a
data frame of version release numbers and dates. The caller is responsible
for extracting out the version components due to the non-standard
semantic versioning used. The \code{\link[=is_valid]{is_valid()}} function can be used to test the
validity of version strings.
}

19
man/mongodb_version_history.Rd

@ -0,0 +1,19 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/mongodb.R
\name{mongodb_version_history}
\alias{mongodb_version_history}
\title{Retrive MongoDB Version Release History}
\usage{
mongodb_version_history()
}
\description{
Reads \url{https://www.mongodb.org/dl/linux"} to build a data frame of
MongoDB version release numbers and dates with semantic version
strings parsed and separate fields added. The data frame is also arranged in
order from lowest version to latest version and the \code{vers} column is an
ordered factor.
}
\note{
Release candidates are not included and release history is only
supported for linux releases.
}

15
man/nginx_version_history.Rd

@ -0,0 +1,15 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/nginx.R
\name{nginx_version_history}
\alias{nginx_version_history}
\title{Retrive nginx Version Release History}
\usage{
nginx_version_history()
}
\description{
Reads \url{https://raw.githubusercontent.com/nginx/nginx/master/docs/xml/nginx/changes.xml}
to build a data frame of nginx version release numbers and dates with semantic version
strings parsed and separate fields added. The data frame is also arranged in
order from lowest version to latest version and the \code{vers} column is an
ordered factor.
}

15
man/sendmail_version_history.Rd

@ -0,0 +1,15 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/sendmail.R
\name{sendmail_version_history}
\alias{sendmail_version_history}
\title{Retrive sendmail Version Release History}
\usage{
sendmail_version_history()
}
\description{
Reads \url{ftp://ftp.sendmail.org/pub/sendmail/"} to build a data frame of
\code{sendmail} version release numbers and dates with semantic version
strings parsed and separate fields added. The data frame is also arranged in
order from lowest version to latest version and the \code{vers} column is an
ordered factor.
}

14
man/vershist.Rd

@ -0,0 +1,14 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/vershist-package.R
\docType{package}
\name{vershist}
\alias{vershist}
\alias{vershist-package}
\title{Collect Version Histories For Vendor Products}
\description{
Provides a set of functions to gather version histories of products
(mainly software products) from their sources (generally websites).
}
\author{
Bob Rudis (bob@rud.is)
}

3
src/.gitignore

@ -0,0 +1,3 @@
*.o
*.so
*.dll

2
src/Makevars

@ -0,0 +1,2 @@
CXX_STD = CXX11
PKG_CXXFLAGS =

28
src/RcppExports.cpp

@ -0,0 +1,28 @@
// Generated by using Rcpp::compileAttributes() -> do not edit by hand
// Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
#include <Rcpp.h>
using namespace Rcpp;
// is_valid
std::vector < bool > is_valid(std::vector < std::string > v);
RcppExport SEXP _vershist_is_valid(SEXP vSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< std::vector < std::string > >::type v(vSEXP);
rcpp_result_gen = Rcpp::wrap(is_valid(v));
return rcpp_result_gen;
END_RCPP
}
static const R_CallMethodDef CallEntries[] = {
{"_vershist_is_valid", (DL_FUNC) &_vershist_is_valid, 1},
{NULL, NULL, 0}
};
RcppExport void R_init_vershist(DllInfo *dll) {
R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
R_useDynamicSymbols(dll, FALSE);
}

218
src/base/type.h

@ -0,0 +1,218 @@
#ifndef CPP_SEMVER_TYPE_HPP
#define CPP_SEMVER_TYPE_HPP
#include <string>
#include <vector>
namespace semver
{
// ------------ exception types ------------------------------------ //
struct semver_error : public std::runtime_error
{
semver_error(const std::string& msg) : std::runtime_error(msg) {}
};
// ------------ syntax types ------------------------------------ //
struct syntax
{
/*
* original NPM semver syntax grammar: https://docs.npmjs.com/misc/Syntax#range-grammar
*
* range-set ::= range ( logical-or range ) *
* logical-or ::= ( ' ' ) * '||' ( ' ' ) *
* range ::= hyphen | simple ( ' ' simple ) * | ''
* hyphen ::= partial ' - ' partial
* simple ::= primitive | partial | tilde | caret
* primitive ::= ( '<' | '>' | '>=' | '<=' | '=' | ) partial
* partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
* xr ::= 'x' | 'X' | '*' | nr
* nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
* tilde ::= '~' partial
* caret ::= '^' partial
* qualifier ::= ( '-' pre )? ( '+' build )?
* pre ::= parts
* build ::= parts
* parts ::= part ( '.' part ) *
* part ::= nr | [-0-9A-Za-z]+
*/
enum class comparator
{
eq, lt, lte, gt, gte, tilde, caret
};
/// default represents '*'
struct xnumber
{
bool is_wildcard = true;
int value = 0;
};
/// represents any type of 'simple', 'primitive', 'partial', 'tilde' or 'caret' from the grammar.
/// The default value represents *.*.*
struct simple
{
xnumber major;
xnumber minor;
xnumber patch;
std::string pre = "";
std::string build = "";
comparator cmp = comparator::eq;
};
/// intersection set (i.e. AND conjunction )
typedef std::vector< simple > range;
/// union set (i.e. OR conjunction )
typedef std::vector< range > range_set;
};
// ------------ semantic types ------------------------------------ //
struct semantic
{
struct boundary
{
int major = 0;
int minor = 0;
int patch = 0;
std::string pre = "";
bool is_max = false;
/// Represent a minimal version boundary
static boundary min()
{
return {};
}
/// Represent a maximal version boundary
static boundary max()
{
boundary b;
b.is_max = true;
return b;
}
};
/// default interval is *.*.* := [min, max]
struct interval
{
bool from_inclusive = true;
bool to_inclusive = true;
boundary from = boundary::min();
boundary to = boundary::max();
};
/// interval set (i.e. OR conjunction )
typedef std::vector< interval > interval_set;
};
bool operator ==(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
return (lhs.major == rhs.major) &&
(lhs.minor == rhs.minor) &&
(lhs.patch == rhs.patch) &&
(lhs.pre == rhs.pre) &&
(lhs.is_max == rhs.is_max);
}
bool operator !=(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
return !(lhs == rhs);
}
bool operator <(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
// TODO: improve the performance of comparison
// XXX: is_max case
if (lhs.is_max || rhs.is_max)
return !lhs.is_max && rhs.is_max; // 1.2.3 < max
if (lhs.major < rhs.major) // 1.* < 2.*
return true;
if ((lhs.major == rhs.major) && // 1.2.* < 1.3.*
(lhs.minor < rhs.minor))
return true;
if ((lhs.major == rhs.major) && // 1.2.3 < 1.2.4
(lhs.minor == rhs.minor) &&
(lhs.patch < rhs.patch))
return true;
// XXX: pre-release case
if ((lhs.major == rhs.major) && // 1.2.3-alpha < 1.2.3
(lhs.minor == rhs.minor) &&
(lhs.patch == rhs.patch) &&
(!lhs.pre.empty() && rhs.pre.empty()))
return true;
// XXX: pre-release case
if ((lhs.major == rhs.major) && // 1.2.3-alpha < 1.2.3-beta
(lhs.minor == rhs.minor) &&
(lhs.patch == rhs.patch) &&
(!lhs.pre.empty() && !rhs.pre.empty()) &&
(lhs.pre < rhs.pre))
return true;
return false;
}
bool operator >(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
return (lhs != rhs) && !(lhs < rhs);
}
bool operator <=(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
return (lhs < rhs) || (lhs == rhs);
}
bool operator >=(const semantic::boundary& lhs, const semantic::boundary& rhs)
{
return (lhs > rhs) || (lhs == rhs);
}
bool operator ==(const semantic::interval& lhs, const semantic::interval& rhs)
{
return (lhs.from_inclusive == rhs.from_inclusive) &&
(lhs.to_inclusive == rhs.to_inclusive) &&
(lhs.from == rhs.from) &&
(lhs.to == rhs.to);
}
bool operator !=(const semantic::interval& lhs, const semantic::interval& rhs)
{
return !(lhs == rhs);
}
bool operator <(const semantic::interval& lhs, const semantic::interval& rhs)
{
if (lhs.to < rhs.from ||
(lhs.to == rhs.from && (!lhs.to_inclusive || !rhs.from_inclusive)))
return true;
return false;
}
bool operator <=(const semantic::interval& lhs, const semantic::interval& rhs)
{
return (lhs < rhs) || (lhs == rhs);
}
bool operator >(const semantic::interval& lhs, const semantic::interval& rhs)
{
return (lhs != rhs) && !(lhs < rhs);
}
bool operator >=(const semantic::interval& lhs, const semantic::interval& rhs)
{
return (lhs > rhs) || (lhs == rhs);
}
}
#endif

69
src/base/util.h

@ -0,0 +1,69 @@
#ifndef CPP_SEMVER_UTIL_HPP
#define CPP_SEMVER_UTIL_HPP
#include <string>
#include <vector>
namespace semver
{
const std::string any_space = " \n\r\t\v\f";
const std::string any_number = "0123456789";
const std::string any_alphabat = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// [ x y z ] -> [ x y z ]
std::string reduce_space(const std::string& str)
{
std::string new_string;
bool pre_space = false;
for (const char& c : str)
{
const bool has_space = (any_space.find(c) != std::string::npos);
if (pre_space && has_space)
continue;
new_string.push_back(c);
pre_space = has_space;
}
return new_string;
}
// [ x y z ] -> [x y z]
std::string trim_string(const std::string& str)
{
size_t start = str.find_first_not_of(any_space);
if (start == std::string::npos)
return {};
size_t end = str.find_last_not_of(any_space);
if (end == std::string::npos)
return str.substr(start);
return str.substr(start, end - start + 1);
}
std::vector<std::string> split(const std::string& input, const std::string& delimiter, bool trim = false)
{
std::vector<std::string> result;
size_t search_pos = 0;
while (1)
{
size_t found = input.find(delimiter, search_pos);
if (found == std::string::npos)
{
result.emplace_back(input.substr(search_pos));
break;
}
result.emplace_back(input.substr(search_pos, found - search_pos));
search_pos = found + delimiter.length();
}
if (trim)
for (std::string& s : result)
s = trim_string(s);
return result;
}
}
#endif

534
src/cpp-semver.h

@ -0,0 +1,534 @@
#ifndef CPP_SEMVER_HPP
#define CPP_SEMVER_HPP
#include "base/type.h"
#include "base/util.h"
#ifdef USE_PEGTL
#include "parser/peg.hpp" // PETGTL parser
#define PARSER peg::parser
#else
#include "parser/parser.h"
#define PARSER semver::parser
#endif
#include <string>
#include <vector>
#include <memory>
namespace semver
{
namespace detail
{
/* parse version range in string into syntactical representation @c syntax::range_set */
syntax::range_set parse(const std::string& input)
{
return PARSER(input);
}
/* parse syntactical representation @c syntax::simple and convert it to sementical representation @c semantic::interval */
semantic::interval parse(const syntax::simple& input)
{
// default result * := [min, max]
semantic::interval result;
semantic::boundary b; // associate boundary from input x.y.z-pre
// replace * with 0
b.major = (!input.major.is_wildcard ? input.major.value : 0);
b.minor = (!input.minor.is_wildcard ? input.minor.value : 0);
b.patch = (!input.patch.is_wildcard ? input.patch.value : 0);
b.pre = input.pre;
switch (input.cmp)
{
case syntax::comparator::lt: // <x.y.z-pre := [min, x.y.z-pre)
result.to_inclusive = false;
result.to = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard) // <x.*-pre := [min, x.0.0)
result.to.pre = ""; // <x.y.*-pre := [min, x,y.0)
break;
case syntax::comparator::lte: // <=x.y.z.pre := [min, x.y.z-pre]
result.to = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard) // <=x.*-pre := [min, 'x+1'.0.0)
{ // <=x.y.*-pre := [min, x,'y+1'.0)
result.to_inclusive = false;
result.to.pre = "";
}
if (input.major.is_wildcard) // <=* := [min, max]
{
result = semantic::interval();
}
else if (input.minor.is_wildcard) // <=x.*-pre := [min, 'x+1'.0.0)
{
result.to.major += 1;
result.to.patch = 0;
}
else if (input.patch.is_wildcard) // <=x.y.*-pre := [min, x,'y+1'.0)
{
result.to.minor += 1;
}
break;
case syntax::comparator::gt: // >x.y.z-pre := (x.y.z-pre, max]
result.from_inclusive = false;
result.from = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard) // >x.*-pre := ['x+1', max]
{ // >x.y.*-pre := [x.'y+1', max]
result.from_inclusive = true;
result.from.pre = "";
}
if (input.major.is_wildcard)
{
result.from = semantic::boundary::max(); // invalid set
}
else if (input.minor.is_wildcard) // >x.*-pre := ['x+1'.0.0, max]
{
result.from.major += 1;
result.from.patch = 0;
}
else if (input.patch.is_wildcard) // >x.y.*-pre := [x,'y+1'.0, max]
{
result.from.minor += 1;
}
break;
case syntax::comparator::gte: // >=x.y.z-pre := [x.y.z-pre, max]
result.from = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard) // >=x.*-pre := [x.0.0, max]
result.from.pre = ""; // >=x.y.*-pre := [x.y.0, max]
break;
case syntax::comparator::caret:
result.from = b;
result.to = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard)
result.from.pre = "";
result.to_inclusive = false;
result.to.pre = "";
if (input.major.is_wildcard) // ^* := [min, max]
{
result = semantic::interval();
}
else if (input.major.value != 0 || // ^x.y.z-pre := [x.y.z-pre, 'x+1'.0.0)
input.minor.is_wildcard) // ^x.y := [x.y.0, 'x+1'.0.0)
{ // ^x := [x.0.0, 'x+1'.0.0)
result.to.major += 1; // ^0 := [0.0.0, 1.0.0)
result.to.minor = 0;
result.to.patch = 0;
}
else if (input.minor.value != 0 || // ^0.y.z := [0.y.z, 0.'y+1'.z)
input.patch.is_wildcard) // ^0.y := [0.y.0, 0.'y+1'.0)
{
result.to.minor += 1;
result.to.patch = 0;
}
else if (input.patch.value != 0)
{
result.to.patch += 1; // ^0.0.z := [0.0.z, 0.0.'z+1')
}
break;
case syntax::comparator::tilde:
result.from = b;
result.to = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard)
result.from.pre = "";
result.to_inclusive = false;
result.to.pre = "";
if (input.major.is_wildcard) // ~* := [min, max]
{
result = semantic::interval();
}
else if (input.minor.is_wildcard) // ~x-pre := [x.0.0, 'x+1'.0.0)
{
result.from.pre = "";
result.to.major += 1;
result.to.minor = 0;
result.to.patch = 0;
}
else // ~x.y.z-pre := [x.y.z-pre, x.'y+1'.0)
{
result.to.minor += 1;
result.to.patch = 0;
}
break;
case syntax::comparator::eq:
default:
result.from = b; // x.y.z := [x.y.z, x.y.z]
result.to = b;
if (input.major.is_wildcard ||
input.minor.is_wildcard ||
input.patch.is_wildcard) // =x.*-pre := [x.0.0, 'x+1'.0.0)
{ // =x.y.*-pre := [x.y.0, x.'y+1'.0)
result.from.pre = "";
result.to_inclusive = false;
result.to.pre = "";
}
if (input.major.is_wildcard) // * := [min, max]
{
result = semantic::interval();
}
else if (input.minor.is_wildcard) // x := [x.0.0, 'x+1'.0.0)
{
result.from.patch = 0;
result.to.major += 1;
result.to.patch = 0;
}
else if (input.patch.is_wildcard) // x.y := [x.y.0, x.'y+1'.0)
{
result.to.minor += 1;
}
}
return result;
};
/* calculate AND-conjunction from a list of @c semantic::interval */
std::unique_ptr<semantic::interval> and_conj(const std::vector< semantic::interval >& input)
{
const size_t size = input.size();
if (size == 0)
return nullptr;
semantic::interval result = input.at(0);
for (size_t i = 1; i < size; i++)
{
const semantic::interval& span = input.at(i);
// prerelease tag case
{
const bool result_is_range = (result.from != result.to || result.from_inclusive != result.to_inclusive);
const bool span_is_range = (span.from != span.to || span.from_inclusive != span.to_inclusive);
if (result_is_range != span_is_range)
{
const semantic::interval& range = (result_is_range ? result : span);
const semantic::interval& target = (result_is_range ? span : result);
const bool range_from_has_pre = !range.from.pre.empty();
const bool range_to_has_pre = !range.to.pre.empty();
const bool target_has_pre = !target.from.pre.empty();
if (target_has_pre)
{
// 1-rc not in '0 - 10'
if (!range_from_has_pre && !range_to_has_pre)
return nullptr;
// 1-rc in '1-beta - 10'
if (range_from_has_pre && !range_to_has_pre)
if (range.from.major != target.from.major ||
range.from.minor != target.from.minor ||
range.from.patch != target.from.patch)
return nullptr;
// 10-beta in '1 - 10-rc'
if (!range_from_has_pre && range_to_has_pre)
if (range.to.major != target.to.major ||
range.to.minor != target.to.minor ||
range.to.patch != target.to.patch)
return nullptr;
// 1-rc or 10-beta in '1-beta - 10-rc'
if (range_from_has_pre && range_to_has_pre)
if (!((range.from.major == target.from.major &&
range.from.minor == target.from.minor &&
range.from.patch == target.from.patch) ||
(range.to.major == target.to.major &&
range.to.minor == target.to.minor &&
range.to.patch == target.to.patch)))
return nullptr;
}
}
} // prerelease tag case
if ((span.from > result.from) ||
((span.from == result.from) && !span.from_inclusive))
{
result.from = span.from;
result.from_inclusive = span.from_inclusive;
}
if ((span.to < result.to) ||
((span.to == result.to) && !span.to_inclusive))
{
result.to = span.to;
result.to_inclusive = span.to_inclusive;
}
}
if ((result.from > result.to) ||
((result.from == result.to) && (result.from_inclusive != result.to_inclusive)))
return nullptr;
return std::unique_ptr<semantic::interval>(new semantic::interval(result));
}
/* parse syntactical representation @c syntax::range_set and convert it to sementical representation @c semantic::interval_set */
semantic::interval_set parse(const syntax::range_set& input)
{
semantic::interval_set or_set;
for (const syntax::range& range : input)
{
std::vector<semantic::interval> and_set;
for (const syntax::simple& simple : range)
and_set.emplace_back(parse(simple));
const std::unique_ptr<semantic::interval> conj = and_conj(and_set);
if (conj)
or_set.emplace_back(std::move(*conj));
}
return or_set;
}
/* check if two @c semantic::interval_set intersect with each other */
bool intersects(const semantic::interval_set& s1, const semantic::interval_set& s2)
{
if (s1.empty() || s2.empty())
return false;
for (const semantic::interval& intval_1 : s1)
for (const semantic::interval& intval_2 : s2)
if (and_conj({ intval_1, intval_2 }))
return true;
return false;
}
/* check if two @c syntax::range_set intersect with each other */
bool intersects(const syntax::range_set& rs1, const syntax::range_set& rs2)
{
return intersects(parse(rs1), parse(rs2));
}
/** parse or cast as a syntax::simple if possible */
syntax::simple as_simple(const std::string& s)
{
syntax::range_set parsed = parse(s);
if (!parsed.empty() && !parsed.at(0).empty())
return std::move(parsed.at(0).at(0));
return{};
}
}
// *************** API **************************************** //
/** Return true if the comparator or range intersect */
bool intersects(const std::string& range)
{
return detail::intersects(detail::parse(range), detail::parse("*"));
}
/** Return true if the two supplied ranges or comparators intersect. */
bool intersects(const std::string& range1, const std::string& range2)
{
return detail::intersects(detail::parse(range1), detail::parse(range2));
}
/** Return true if the version satisfies the range */
bool satisfies(const std::string& version, const std::string& range)
{
return detail::intersects(detail::parse(version), detail::parse(range));
}
/** v1 == v2 */
bool eq(const std::string& v1, const std::string& v2)
{
semantic::interval_set interval_set_1 = detail::parse(detail::parse(v1));
semantic::interval_set interval_set_2 = detail::parse(detail::parse(v2));
if (interval_set_1.empty() && interval_set_2.empty())
return true;
else if (interval_set_1.empty() != interval_set_2.empty())
return false;
for (const semantic::interval& interval_1 : interval_set_1)
for (const semantic::interval& interval_2 : interval_set_2)
if (interval_1 == interval_2)
return true;
return false;
}
/** v1 != v2 */
bool neq(const std::string& v1, const std::string& v2)
{
return !eq(v1, v2);
}
/** v1 > v2 */
bool gt(const std::string& v1, const std::string& v2)
{
semantic::interval_set interval_set_1 = detail::parse(detail::parse(v1));
semantic::interval_set interval_set_2 = detail::parse(detail::parse(v2));
if (!interval_set_1.empty() && interval_set_2.empty())
return true;
else if (interval_set_1.empty() || interval_set_2.empty())
return false;
for (const semantic::interval& interval_1 : interval_set_1)
for (const semantic::interval& interval_2 : interval_set_2)
if (interval_1 > interval_2)
return true;
return false;
}
/** v1 >= v2 */
bool gte(const std::string& v1, const std::string& v2)
{
if (eq(v1, v2) || gt(v1, v2))
return true;
return false;
}
/** v1 < v2 */
bool lt(const std::string& v1, const std::string& v2)
{
if (!eq(v1, v2) && !gt(v1, v2))
return true;
return false;
}
/** v1 <= v2 */
bool lte(const std::string& v1, const std::string& v2)
{
if (eq(v1, v2) || lt(v1, v2))
return true;
return false;
}
/** Return true if version is greater than all the versions possible in the range. */
bool gtr(const std::string& version, const std::string& range)
{
semantic::interval_set interval_set_v = detail::parse(detail::parse(version));
semantic::interval_set interval_set_r = detail::parse(detail::parse(range));
if (!interval_set_v.empty() && interval_set_r.empty())
return true;
else if (interval_set_v.empty() || interval_set_r.empty())
return false;
for (const semantic::interval& interval_v : interval_set_v)
for (const semantic::interval& interval_r : interval_set_r)
if (interval_v <= interval_r)
return false;
return true;
}
/** Return true if version is less than all the versions possible in the range. */
bool ltr(const std::string& version, const std::string& range)
{
semantic::interval_set interval_set_v = detail::parse(detail::parse(version));
semantic::interval_set interval_set_r = detail::parse(detail::parse(range));
if (interval_set_v.empty() && !interval_set_r.empty())
return true;
else if (interval_set_v.empty() || interval_set_r.empty())
return false;
for (const semantic::interval& interval_v : interval_set_v)
for (const semantic::interval& interval_r : interval_set_r)
if (interval_v >= interval_r)
return false;
return true;
}
/** Return true if the version or range is valid */
bool valid(const std::string& s)
{
try
{
semantic::interval_set interval_set_s = detail::parse(detail::parse(s));
return !interval_set_s.empty();
}
catch (semver_error const&)
{
return false;
}
}
/** Return the major version number. */
int major(const std::string& version)
{
const auto& major = detail::as_simple(version).major;
return !major.is_wildcard ? major.value : 0;
}
/** Return the minor version number. */
int minor(const std::string& version)
{
const auto& minor = detail::as_simple(version).minor;
return !minor.is_wildcard ? minor.value : 0;
}
/** Return the patch version number. */
int patch(const std::string& version)
{
const auto& patch = detail::as_simple(version).patch;
return !patch.is_wildcard ? patch.value : 0;
}
/** Returns an array of prerelease components. */
std::vector<std::string> prerelease(const std::string& version)
{
const auto& pre = detail::as_simple(version).pre;
if (pre.empty())
return {};
return split(pre, ".");
}
}
#endif

230
src/parser/parser.h

@ -0,0 +1,230 @@
#ifndef CPP_SEMVER_PARSER_HPP
#define CPP_SEMVER_PARSER_HPP
#include "../base/type.h"
#include "../base/util.h"
#include <vector>
#include <string>
namespace semver
{
int parse_nr(const std::string& input)
{
// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
if (input.length() == 0)
throw semver_error("empty string as invalid number");
if (input.find_first_not_of(any_number) != std::string::npos)
throw semver_error("unexpected char as invalid number: '" + input + "'");
if (input.length() > 1 && input.at(0) == '0')
throw semver_error("unexpected '0' as invalid number: '" + input + "'");
try
{
return std::stoi(input);
}
catch (std::invalid_argument&)
{
throw semver_error("invalid number: '" + input + "'");
}
}
syntax::xnumber parse_xr(const std::string& input)
{
// xr ::=
// 'x' | 'X' | '*' |
if (input == "x" || input == "X" || input == "*")
return {};
// nr
syntax::xnumber nr;
nr.is_wildcard = false;
nr.value = parse_nr(input);
return nr;
}
std::string parse_part(const std::string& input)
{
// part ::=
if (input.empty() || any_number.find(input.at(0)) != std::string::npos)
{ // nr |
parse_nr(input);
}
else if (input.find_first_not_of("-" + any_num