diff --git a/DESCRIPTION b/DESCRIPTION index 3d3cd6e..b74d541 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: audit.base Title: Base package for Posit Checks -Version: 0.7.8 +Version: 0.7.9 Authors@R: person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre")) Description: Base package for sharing classes between posit audit diff --git a/NEWS.md b/NEWS.md index 0f52e04..bbdb423 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +# audit.base 0.7.9 _2026-10-16_ + * fix: Posit versions are compared as numbers, so a patch release newer than the list or missing from it is reported as current or out of date, not "really old", and 2030.x versions are recognised. The scrape fallback warning gives the reason, and `get_quarto_posit_version_msg()` returns one message when `type` is left at its default (#62) + # audit.base 0.7.8 _2026-10-16_ * fix: `audit_posit_version()` and `get_quarto_posit_version_msg()` say the Posit version couldn't be found when it's `NA`, instead of erroring (jumpingrivers/audit.workbench#38) diff --git a/R/get_posit_remote_versions.R b/R/get_posit_remote_versions.R index 5c0507e..438da93 100644 --- a/R/get_posit_remote_versions.R +++ b/R/get_posit_remote_versions.R @@ -44,6 +44,6 @@ get_all_remote_versions = function(page) { versions = page %>% rvest::html_nodes("h2") %>% rvest::html_text() - v = stringr::str_extract(versions, "202[0-9]\\.[0-9]{2}\\.[0-9]{1,2}") + v = stringr::str_extract(versions, "20[0-9]{2}\\.[0-9]{2}\\.[0-9]{1,2}") tibble::tibble(version = v[!is.na(v)], cve = "") } diff --git a/R/posit_versions.R b/R/posit_versions.R index 6d2956c..9299620 100644 --- a/R/posit_versions.R +++ b/R/posit_versions.R @@ -13,17 +13,19 @@ get_posit_versions = function( remote = TRUE ) { type = match.arg(type) - versions = if (isTRUE(remote) && type != "drivers") { - try(get_posit_remote_versions(type), silent = TRUE) - } else { - NULL + versions = NULL + if (isTRUE(remote) && type != "drivers") { + versions = tryCatch( + get_posit_remote_versions(type), + error = function(e) { + cli::cli_alert_warning( + "Unable to scrape NEWS page for {type}: {conditionMessage(e)}. Falling back to cache" + ) + NULL + } + ) } - if ("try-error" %in% class(versions) || is.null(versions)) { - if (type != "drivers") { - cli::cli_alert_warning( - "Unable to scrape NEWS page for {type}. Falling back to cache" - ) - } + if (is.null(versions)) { fname = system.file( "extdata", "versions", @@ -35,7 +37,8 @@ get_posit_versions = function( versions = utils::read.csv(fname, header = TRUE, comment.char = "#", colClasses = "character") tibble::as_tibble(versions) } - versions = dplyr::arrange(versions, dplyr::desc(.data$version)) + # Newest first, compared as numbers so 2024.12.10 sorts above 2024.12.2 + versions = versions[order(numeric_version(versions$version), decreasing = TRUE), ] # Versions without a CVE have an empty string; make these NA so they aren't counted versions$cve = dplyr::na_if(trimws(as.character(versions$cve)), "") versions @@ -64,7 +67,7 @@ audit_posit_version = function( return(invisible(NULL)) } versions = get_posit_versions(type = type) - row_number = lookup_version(posit_version, type = type) + row_number = lookup_version(posit_version, versions) if (is.na(row_number)) { cli::cli_alert_info( @@ -85,45 +88,28 @@ audit_posit_version = function( invisible(NULL) } -lookup_version = function(posit_version, type) { - versions = get_posit_versions(type = type) - version_as_date = version_to_date(posit_version) - all_dates = unlist(purrr::map(versions$version, version_to_date)) - - if (is.na(version_as_date) || version_as_date < min(all_dates)) { - # Older than DB - row_number = NA_integer_ - } else if (version_as_date > max(all_dates)) { - # Newer than DB - row_number = 1L - } else { - # Return matching version; multiple CVEs, so just pick 1 - # Rtns an NA when not in the DB - # Some versions add on additional info, .e.g. 2023.06.0.pro1 - delete - posit_version = stringr::str_extract( - posit_version, - "^(202[0-9])\\.([01][0-9])\\.[0-9]" - ) - row_number = which(versions$version == posit_version)[1] +# Row of `versions` (sorted newest first) that `posit_version` sits at: +# 1 when it's at or above the newest entry, otherwise one more than the number of +# strictly newer versions. NA when it can't be parsed or predates the list. +lookup_version = function(posit_version, versions) { + version = parse_posit_version(posit_version) + all_versions = numeric_version(versions$version) + if (is.na(version) || version < min(all_versions)) { + return(NA_integer_) } - row_number + sum(all_versions > version) + 1L } -version_to_date = function(version) { - # Old style version - if (is_missing_version(version) || !is_new_version(version)) { +# Year.month.patch, ignoring suffixes such as .pro1; NA for old style versions (1.9.0) +parse_posit_version = function(version) { + if (is_missing_version(version)) { return(NA) } - as_date = stringr::str_match_all(version, "^(202[0-9])\\.([01][0-9])")[[1]] - as.Date(paste(as_date[1, 2], as_date[1, 3], "01", sep = "-")) + version = stringr::str_extract(version, "^20[0-9]{2}\\.[01][0-9]\\.[0-9]+") + if (is.na(version)) NA else numeric_version(version) } # NA or character(0) when the version couldn't be found is_missing_version = function(version) { length(version) != 1L || is.na(version) } - -# Old style versions are 1.9.0 (2021-07) -is_new_version = function(version) { - stringr::str_detect(version, pattern = "^202[0-9]\\.[01][0-9].[0-9]") -} diff --git a/R/quarto-helpers.R b/R/quarto-helpers.R index ca12f6c..6239960 100644 --- a/R/quarto-helpers.R +++ b/R/quarto-helpers.R @@ -75,23 +75,24 @@ get_quarto_posit_version_msg = function( out, type = c("connect", "workbench", "drivers") ) { + type = match.arg(type) posit_version = out$posit_version if (is_missing_version(posit_version)) { return(glue::glue("We couldn't find which version of Posit {type} is installed.")) } - row_number = lookup_version(posit_version, type) + versions = get_posit_versions(type = type) + row_number = lookup_version(posit_version, versions) status = version_status(row_number) if (status == "unknown") { msg = "Posit {type} (v{posit_version}), isn't in our database. This could be because we've missed it or it's really old." } else if (status == "outdated") { - versions = get_posit_versions(type = type) newer_versions = versions[seq_len(row_number - 1), ] # nolint no_of_versions = length(unique(newer_versions$version)) # nolint no_of_cves = count_cves(newer_versions) # nolint msg = "Posit {type} is out of date (v{posit_version}). There are {no_of_versions} newer versions that fix {no_of_cves} CVEs. - The latest version is v{versions[1, 1]}." + The latest version is v{versions$version[1]}." } else { msg = "Posit {type} is up to date." } @@ -107,7 +108,8 @@ get_quarto_posit_version_status = function( out, type = c("connect", "workbench", "drivers") ) { - version_status(lookup_version(out$posit_version, type)) + type = match.arg(type) + version_status(lookup_version(out$posit_version, get_posit_versions(type = type))) } version_status = function(row_number) { diff --git a/tests/testthat/test-posit_versions.R b/tests/testthat/test-posit_versions.R index 904ed94..96e3496 100644 --- a/tests/testthat/test-posit_versions.R +++ b/tests/testthat/test-posit_versions.R @@ -20,23 +20,6 @@ test_that("Testing check server", { regexp = "up to date" ) - # Version not in DB - expect_equal(lookup_version(posit_version = "2029.01.01", type), 1) - # Really old version not in the DB - expect_equal( - lookup_version(posit_version = "2010.01.01", type), - NA_integer_ - ) - - expect_equal( - lookup_version(posit_version = "Not in DB", type), - NA_integer_ - ) - - # No version in DB - v = lookup_version(posit_version = "2022.10.31", type) - expect_true(is.na(v)) - expect_message(audit_posit_version("2022.10.0", type), regexp = "out of date") }) @@ -81,3 +64,26 @@ test_that("A version that couldn't be found is reported, not an error", { expect_match(msg, "couldn't find") } }) + +test_that("lookup_version() places a version against the list", { + versions = tibble::tibble(version = c("2026.10.0", "2026.09.0", "2026.08.2", "2026.08.0")) + cases = tibble::tribble( + ~posit_version, ~row, + "2026.10.0", 1L, + "2026.10.1", 1L, # patch after the newest entry + "2026.10.0.pro1", 1L, + "2030.01.0", 1L, + "2026.08.3", 3L, # patch missing from the list + "2026.08.0", 4L, + "2025.01.0", NA, # older than the list + "1.9.0", NA, + "Not a version", NA + ) + for (i in seq_len(nrow(cases))) { + expect_identical( + lookup_version(cases$posit_version[i], versions), + cases$row[i], + label = cases$posit_version[i] + ) + } +})