From d607d2c6be1d4c8bdf15530da984760b62bdc6ca Mon Sep 17 00:00:00 2001 From: boB Rudis Date: Tue, 20 Mar 2018 16:49:26 -0400 Subject: [PATCH] initial commit --- .Rbuildignore | 11 + .codecov.yml | 1 + .gitignore | 8 + .travis.yml | 6 + DESCRIPTION | 36 +++ NAMESPACE | 42 +++ NEWS.md | 3 + R/RcppExports.R | 11 + R/aaa.R | 6 + R/apache-httpd.R | 29 ++ R/lighttpd.R | 35 +++ R/mongodb.R | 44 +++ R/nginx.R | 34 +++ R/sendmail.R | 36 +++ R/vershist-package.R | 25 ++ README.Rmd | 77 ++++++ README.md | 153 +++++++++++ man/apache_httpd_version_history.Rd | 15 + man/is_valid.Rd | 14 + man/lighttpd_version_history.Rd | 15 + man/mongodb_version_history.Rd | 19 ++ man/nginx_version_history.Rd | 15 + man/sendmail_version_history.Rd | 15 + man/vershist.Rd | 14 + src/.gitignore | 3 + src/Makevars | 2 + src/RcppExports.cpp | 28 ++ src/base/type.h | 218 +++++++++++++++ src/base/util.h | 69 +++++ src/cpp-semver.h | 534 ++++++++++++++++++++++++++++++++++++ src/parser/parser.h | 230 ++++++++++++++++ src/vershist-main.cpp | 33 +++ tests/test-all.R | 2 + tests/testthat/test-vershist.R | 6 + vershist.Rproj | 21 ++ 35 files changed, 1810 insertions(+) create mode 100644 .Rbuildignore create mode 100644 .codecov.yml create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 DESCRIPTION create mode 100644 NAMESPACE create mode 100644 NEWS.md create mode 100644 R/RcppExports.R create mode 100644 R/aaa.R create mode 100644 R/apache-httpd.R create mode 100644 R/lighttpd.R create mode 100644 R/mongodb.R create mode 100644 R/nginx.R create mode 100644 R/sendmail.R create mode 100644 R/vershist-package.R create mode 100644 README.Rmd create mode 100644 README.md create mode 100644 man/apache_httpd_version_history.Rd create mode 100644 man/is_valid.Rd create mode 100644 man/lighttpd_version_history.Rd create mode 100644 man/mongodb_version_history.Rd create mode 100644 man/nginx_version_history.Rd create mode 100644 man/sendmail_version_history.Rd create mode 100644 man/vershist.Rd create mode 100644 src/.gitignore create mode 100644 src/Makevars create mode 100644 src/RcppExports.cpp create mode 100644 src/base/type.h create mode 100644 src/base/util.h create mode 100644 src/cpp-semver.h create mode 100644 src/parser/parser.h create mode 100644 src/vershist-main.cpp create mode 100644 tests/test-all.R create mode 100644 tests/testthat/test-vershist.R create mode 100644 vershist.Rproj diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..70baf05 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,11 @@ +^.*\.Rproj$ +^\.Rproj\.user$ +^\.travis\.yml$ +^README\.*Rmd$ +^README\.*html$ +^NOTES\.*Rmd$ +^NOTES\.*html$ +^\.codecov\.yml$ +^README_files$ +^doc$ +^tmp$ diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..69cb760 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cce1f17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.Rproj.user +.Rhistory +.RData +.Rproj +src/*.o +src/*.so +src/*.dll diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f93993f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: R +sudo: false +cache: packages + +after_success: +- Rscript -e 'covr::codecov()' diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..799eb89 --- /dev/null +++ b/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 +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 diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..96fec09 --- /dev/null +++ b/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) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..2874309 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +0.1.0 +* Initial release +* Support for Apache httpd, nginx, sendmail, lighttpd and mongodb \ No newline at end of file diff --git a/R/RcppExports.R b/R/RcppExports.R new file mode 100644 index 0000000..6a6ec6c --- /dev/null +++ b/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) +} + diff --git a/R/aaa.R b/R/aaa.R new file mode 100644 index 0000000..255cd63 --- /dev/null +++ b/R/aaa.R @@ -0,0 +1,6 @@ +utils::globalVariables( + c( + ".", "vers", "major", "minor", "patch", "product", "rls_date", "rls_year", + "V1", "V2", "V3", "ts" + ) +) \ No newline at end of file diff --git a/R/apache-httpd.R b/R/apache-httpd.R new file mode 100644 index 0000000..2f99054 --- /dev/null +++ b/R/apache-httpd.R @@ -0,0 +1,29 @@ +#' Retrive Apache httpd Version Release History +#' +#' Reads 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("\"[ ", "") %>% + 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)) + +} diff --git a/R/lighttpd.R b/R/lighttpd.R new file mode 100644 index 0000000..f1f3a54 --- /dev/null +++ b/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) + +} \ No newline at end of file diff --git a/R/mongodb.R b/R/mongodb.R new file mode 100644 index 0000000..d4be18b --- /dev/null +++ b/R/mongodb.R @@ -0,0 +1,44 @@ +#' Retrive MongoDB Version Release History +#' +#' Reads 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) + +} diff --git a/R/nginx.R b/R/nginx.R new file mode 100644 index 0000000..0ef3deb --- /dev/null +++ b/R/nginx.R @@ -0,0 +1,34 @@ +#' Retrive nginx Version Release History +#' +#' Reads +#' 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) + +} \ No newline at end of file diff --git a/R/sendmail.R b/R/sendmail.R new file mode 100644 index 0000000..b06ad56 --- /dev/null +++ b/R/sendmail.R @@ -0,0 +1,36 @@ +#' Retrive sendmail Version Release History +#' +#' Reads 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)) + +} diff --git a/R/vershist-package.R b/R/vershist-package.R new file mode 100644 index 0000000..8cecae5 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/README.Rmd b/README.Rmd new file mode 100644 index 0000000..586c18c --- /dev/null +++ b/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() +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..80de342 --- /dev/null +++ b/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 + ## + ## 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 + ## + ## 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 + ## + ## 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 + ## + ## 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 + ## + ## 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 "" "" diff --git a/man/apache_httpd_version_history.Rd b/man/apache_httpd_version_history.Rd new file mode 100644 index 0000000..d9d91df --- /dev/null +++ b/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. +} diff --git a/man/is_valid.Rd b/man/is_valid.Rd new file mode 100644 index 0000000..c456e40 --- /dev/null +++ b/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 +} diff --git a/man/lighttpd_version_history.Rd b/man/lighttpd_version_history.Rd new file mode 100644 index 0000000..c03b4ec --- /dev/null +++ b/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. +} diff --git a/man/mongodb_version_history.Rd b/man/mongodb_version_history.Rd new file mode 100644 index 0000000..a7702ee --- /dev/null +++ b/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. +} diff --git a/man/nginx_version_history.Rd b/man/nginx_version_history.Rd new file mode 100644 index 0000000..b896a1e --- /dev/null +++ b/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. +} diff --git a/man/sendmail_version_history.Rd b/man/sendmail_version_history.Rd new file mode 100644 index 0000000..dd6c8d1 --- /dev/null +++ b/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. +} diff --git a/man/vershist.Rd b/man/vershist.Rd new file mode 100644 index 0000000..e84641d --- /dev/null +++ b/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) +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..22034c4 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,3 @@ +*.o +*.so +*.dll diff --git a/src/Makevars b/src/Makevars new file mode 100644 index 0000000..afb82c3 --- /dev/null +++ b/src/Makevars @@ -0,0 +1,2 @@ +CXX_STD = CXX11 +PKG_CXXFLAGS = diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp new file mode 100644 index 0000000..790bcc9 --- /dev/null +++ b/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 + +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); +} diff --git a/src/base/type.h b/src/base/type.h new file mode 100644 index 0000000..2f2ee0e --- /dev/null +++ b/src/base/type.h @@ -0,0 +1,218 @@ +#ifndef CPP_SEMVER_TYPE_HPP +#define CPP_SEMVER_TYPE_HPP + +#include +#include + +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 \ No newline at end of file diff --git a/src/base/util.h b/src/base/util.h new file mode 100644 index 0000000..4d5c809 --- /dev/null +++ b/src/base/util.h @@ -0,0 +1,69 @@ +#ifndef CPP_SEMVER_UTIL_HPP +#define CPP_SEMVER_UTIL_HPP + +#include +#include + +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 split(const std::string& input, const std::string& delimiter, bool trim = false) + { + std::vector 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 \ No newline at end of file diff --git a/src/cpp-semver.h b/src/cpp-semver.h new file mode 100644 index 0000000..6af8bcf --- /dev/null +++ b/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 +#include +#include + +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 := (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 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(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 and_set; + for (const syntax::simple& simple : range) + and_set.emplace_back(parse(simple)); + const std::unique_ptr 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 prerelease(const std::string& version) + { + const auto& pre = detail::as_simple(version).pre; + if (pre.empty()) + return {}; + + return split(pre, "."); + } + +} + +#endif \ No newline at end of file diff --git a/src/parser/parser.h b/src/parser/parser.h new file mode 100644 index 0000000..fb25d64 --- /dev/null +++ b/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 +#include + +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_number + any_alphabat) != std::string::npos) + { + // [-0-9A-Za-z]+ + throw semver_error("unexpected character in part: '" + input + "'"); + } + + return input; + } + + std::string parse_parts(const std::string& input) + { + // parts ::= part ( '.' part ) * + + std::vector part_tokens = split(input, "."); + for (const std::string& part_token : part_tokens) + parse_part(part_token); + + return input; + } + + syntax::simple parse_partial(const std::string& input) + { + // partial ::= xr ( '.' xr ( '.' xr ( '-' pre )? ( '+' build )? ? )? )? + // pre ::= parts + // build ::= parts + + size_t found_pre = std::string::npos; + const size_t found_build = input.find_first_of('+'); + const size_t found_dash = input.find_first_of('-'); + + if (found_build != std::string::npos && found_dash != std::string::npos) + { + if (found_dash < found_build) + found_pre = found_dash; + } + else if (found_build == std::string::npos && found_dash != std::string::npos) + { + found_pre = found_dash; + } + + syntax::simple result; + + { + if (found_build != std::string::npos) + result.build = parse_parts(input.substr(found_build + 1)); + + if (found_pre != std::string::npos) + { + if (found_build != std::string::npos) + result.pre = parse_parts(input.substr(found_pre + 1, found_build - found_pre - 1)); + else + result.pre = parse_parts(input.substr(found_pre + 1)); + } + } + + { + size_t xr_end = found_pre; + if (xr_end == std::string::npos && found_build != std::string::npos) + xr_end = found_build; + + const std::string xr_xr_xr = (xr_end == std::string::npos) ? input : input.substr(0, xr_end); + + std::vector xr_tokens = split(xr_xr_xr, "."); + if ((!result.pre.empty() || !result.build.empty()) && xr_tokens.size() != 3) + throw semver_error("incomplete version with pre or build tag: '" + xr_xr_xr + "'"); + + if (xr_tokens.size() > 0) + { + // allow 'v' prefix + std::string xr = xr_tokens.at(0); + xr = (xr.find_first_of("vV") == 0) ? xr.substr(1) : xr; + result.major = parse_xr(xr); + } + if (xr_tokens.size() > 1) + result.minor = parse_xr(xr_tokens.at(1)); + if (xr_tokens.size() > 2) + result.patch = parse_xr(xr_tokens.at(2)); + if (xr_tokens.size() > 3) + throw semver_error("invalid version: '" + xr_xr_xr + "'"); + } + + return result; + } + + syntax::simple parse_simple(const std::string& input) + { + // simple ::= primitive | partial | tilde | caret + // primitive ::= ( '<' | '>' | '>=' | '<=' | '=' | ) partial + // tilde ::= '~' partial + // caret ::= '^' partial + + const size_t partial_start = input.find_first_not_of("<>=~^"); + + if (partial_start == std::string::npos) + throw semver_error("invalid version: '" + input + "'"); + + const std::string prefix = input.substr(0, partial_start); + syntax::simple result = parse_partial(input.substr(partial_start)); + + if (prefix == "=" || prefix.empty()) + result.cmp = syntax::comparator::eq; + else if (prefix == "<") + result.cmp = syntax::comparator::lt; + else if (prefix == ">") + result.cmp = syntax::comparator::gt; + else if (prefix == "<=") + result.cmp = syntax::comparator::lte; + else if (prefix == ">=") + result.cmp = syntax::comparator::gte; + else if (prefix == "~") + result.cmp = syntax::comparator::tilde; + else if (prefix == "^") + result.cmp = syntax::comparator::caret; + else + throw semver_error("invalid operator: '" + prefix + "'"); + + return result; + } + + syntax::range parse_range(const std::string& input) + { + // range ::= + std::vector hyphen_tokens = split(input, " - ", true); + if (hyphen_tokens.size() == 2) + { + // hyphen | + syntax::range hyphen; + syntax::simple from = parse_partial(hyphen_tokens.at(0)); + syntax::simple to = parse_partial(hyphen_tokens.at(1)); + from.cmp = syntax::comparator::gte; + to.cmp = syntax::comparator::lte; + hyphen.emplace_back(from); + hyphen.emplace_back(to); + return hyphen; + } + else if (input.find_first_not_of(any_space) != std::string::npos) + { + // simple ( ' ' simple ) * | + std::vector simple_tokens = split(reduce_space(input), " ", true); + syntax::range simples; + for (const std::string& simple_token : simple_tokens) + simples.emplace_back(parse_simple(simple_token)); + return simples; + } + else + { + // '' + // the input may be a blank string which is allowed as an implcit *.*.* range + syntax::range implicit_set; + implicit_set.emplace_back(syntax::simple()); + return implicit_set; + } + } + + syntax::range_set parse_range_set(const std::string& input) + { + syntax::range_set result; + + // range_set ::= range ( ( ' ' ) * '||' ( ' ' ) * range ) * + std::vector range_tokens = split(input, "||", true); + for (const std::string& range_token : range_tokens) + result.emplace_back(parse_range(range_token)); + + return result; + } + + syntax::range_set parser(const std::string input) + { + return parse_range_set(input); + } + +} + +#endif diff --git a/src/vershist-main.cpp b/src/vershist-main.cpp new file mode 100644 index 0000000..ca360e6 --- /dev/null +++ b/src/vershist-main.cpp @@ -0,0 +1,33 @@ +#include +#include + +#include "cpp-semver.h" + +using namespace Rcpp; + +bool one_is_valid(std::string v) { + + try { + return(semver::intersects(v)); + } catch(...) { + return(FALSE); + } + +} + +//' Test if semantic version strings are valid +//' +//' @param v character verctor of version strings +//' @export +// [[Rcpp::export]] +std::vector < bool > is_valid(std::vector < std::string > v) { + + std::vector < bool > ret(v.size()); + + for (unsigned int i = 0; i < v.size(); i++) { + ret[i] = one_is_valid(v[i]); + } + + return(ret); + +} diff --git a/tests/test-all.R b/tests/test-all.R new file mode 100644 index 0000000..96ead1d --- /dev/null +++ b/tests/test-all.R @@ -0,0 +1,2 @@ +library(testthat) +test_check("vershist") diff --git a/tests/testthat/test-vershist.R b/tests/testthat/test-vershist.R new file mode 100644 index 0000000..0c22968 --- /dev/null +++ b/tests/testthat/test-vershist.R @@ -0,0 +1,6 @@ +context("minimal package functionality") +test_that("we can do something", { + + #expect_that(some_function(), is_a("data.frame")) + +}) diff --git a/vershist.Rproj b/vershist.Rproj new file mode 100644 index 0000000..446d9e1 --- /dev/null +++ b/vershist.Rproj @@ -0,0 +1,21 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +StripTrailingWhitespace: Yes + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageBuildArgs: --resave-data +PackageRoxygenize: rd,collate,namespace