blob: f354944f5df770a50fe7e43a7919af3771fefbb4 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{Result, bail};
use camino::Utf8PathBuf;
use regex::Regex;
use std::fs;
use std::sync::OnceLock;
// A helper function to get the compiled regex, describing a valid string
// that can be used when communicating with the upstream versioning service.
// The regex is initialized only on the first call.
fn valid_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9./_-]{0,63}$").unwrap())
}
/// Return an error of the given string does not satisfy the string constraints
/// in upstream versioning servers.
pub fn validate_string_for_upstream_versioning(candidate: String) -> Result<String> {
if valid_regex().is_match(&candidate) {
Ok(candidate)
} else {
bail!(
"Invalid version string: \"{}\". The string must be 1-63 characters long, start with an alphanumeric character, and only contain alphanumeric characters, '.', '_', '-', or '/'.",
candidate
)
}
}
/// Return the "version" string if it is provided.
/// If not, return the contents of the file located at the path "version_file".
/// If neither argument is provided, return the string "unversioned".
pub fn get_release_version(
version: &Option<String>,
version_file: &Option<Utf8PathBuf>,
) -> Result<String> {
get_string_or_file_content(
version,
version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
}
/// Return the "repo" string if it is provided.
/// If not, return the contents of the file located at the path "repo_file".
/// If neither argument is provided, return the string "unknown".
pub fn get_release_repository(
repo: &Option<String>,
repo_file: &Option<Utf8PathBuf>,
) -> Result<String> {
get_string_or_file_content(
repo,
repo_file,
"unknown",
"repo and repo_file cannot both be supplied",
)
}
fn get_string_or_file_content(
field: &Option<String>,
field_file: &Option<Utf8PathBuf>,
undefined_message: &str,
both_defined_message: &str,
) -> Result<String> {
let s = match (field, field_file) {
(None, None) => undefined_message.to_string(),
(Some(_), Some(_)) => bail!(both_defined_message.to_string()),
(Some(field), _) => field.to_string(),
(None, Some(field_file)) => {
let s = fs::read_to_string(field_file)?;
s.trim().to_string()
}
};
if &s == "" {
return Ok(undefined_message.to_string());
}
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8Path;
use std::io::Write;
use tempfile;
#[test]
fn test_default() {
let version: Option<String> = None;
let version_file: Option<Utf8PathBuf> = None;
let version = get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.unwrap();
assert_eq!("unversioned".to_string(), version);
}
#[test]
fn test_version_string() {
let version: Option<String> = Some("version_string".to_string());
let version_file: Option<Utf8PathBuf> = None;
let version = get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.unwrap();
assert_eq!("version_string".to_string(), version);
}
#[test]
fn test_empty_version_string() {
let version: Option<String> = Some("".to_string());
let version_file: Option<Utf8PathBuf> = None;
let version = get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.unwrap();
assert_eq!("unversioned".to_string(), version);
}
#[test]
fn test_version_file() {
let mut file = tempfile::NamedTempFile::new().unwrap();
write!(&mut file, "version_file").unwrap();
let version: Option<String> = None;
let version_file: Option<Utf8PathBuf> =
Some(Utf8Path::from_path(file.path()).unwrap().into());
let version = get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.unwrap();
assert_eq!("version_file".to_string(), version);
}
#[test]
fn test_empty_version_file() {
let mut file = tempfile::NamedTempFile::new().unwrap();
write!(&mut file, "").unwrap();
let version: Option<String> = None;
let version_file: Option<Utf8PathBuf> =
Some(Utf8Path::from_path(file.path()).unwrap().into());
let version = get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.unwrap();
assert_eq!("unversioned".to_string(), version);
}
#[test]
fn test_error_for_both_versions() {
let mut file = tempfile::NamedTempFile::new().unwrap();
write!(&mut file, "version_file").unwrap();
let version: Option<String> = Some("version_string".to_string());
let version_file: Option<Utf8PathBuf> =
Some(Utf8Path::from_path(file.path()).unwrap().into());
assert!(
get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.is_err()
);
}
#[test]
fn test_version_file_missing() {
let version: Option<String> = None;
let version_file: Option<Utf8PathBuf> = Some(Utf8PathBuf::new());
assert!(
get_string_or_file_content(
&version,
&version_file,
"unversioned",
"version and version_file cannot both be supplied",
)
.is_err()
);
}
#[test]
fn test_validate_string() {
let s = "0123579abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_".to_string();
assert!(validate_string_for_upstream_versioning(s).is_ok());
}
#[test]
fn test_validate_string_forward_slash() {
let s = "a/b".to_string();
assert!(validate_string_for_upstream_versioning(s).is_ok());
}
#[test]
fn test_validate_string_too_long() {
let s = "01234567890123456789012345678901234567890123456789012345678901234".to_string();
assert_eq!(s.len(), 65);
assert!(validate_string_for_upstream_versioning(s).is_err());
}
#[test]
fn test_validate_string_invalid_first_char() {
let s = ".a".to_string();
assert!(validate_string_for_upstream_versioning(s).is_err());
}
#[test]
fn test_validate_string_invalid_char() {
let s = "a?b".to_string();
assert!(validate_string_for_upstream_versioning(s).is_err());
}
}