use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ACString {
pub version: u8,
pub consented_atps: Vec<u16>,
pub disclosed_atps: Vec<u16>,
}
impl ACString {
#[must_use]
pub fn new() -> Self {
Self {
version: 2,
consented_atps: Vec::new(),
disclosed_atps: Vec::new(),
}
}
#[must_use]
pub fn with_atps(consented: Vec<u16>, disclosed: Vec<u16>) -> Self {
Self {
version: 2,
consented_atps: consented,
disclosed_atps: disclosed,
}
}
#[must_use]
pub fn has_consent(&self, atp_id: u16) -> bool {
self.consented_atps.contains(&atp_id)
}
#[must_use]
pub fn is_disclosed(&self, atp_id: u16) -> bool {
self.has_consent(atp_id) || self.disclosed_atps.contains(&atp_id)
}
#[must_use]
pub fn all_atps(&self) -> Vec<u16> {
let mut all = self.consented_atps.clone();
all.extend_from_slice(&self.disclosed_atps);
all.sort_unstable();
all.dedup();
all
}
#[must_use]
pub fn consented_count(&self) -> usize {
self.consented_atps.len()
}
#[must_use]
pub fn disclosed_count(&self) -> usize {
self.disclosed_atps.len()
}
#[must_use]
pub fn total_count(&self) -> usize {
self.all_atps().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.consented_atps.is_empty() && self.disclosed_atps.is_empty()
}
#[must_use]
pub fn is_legacy(&self) -> bool {
self.version == 1
}
#[must_use]
pub fn all_disclosed_consented(&self) -> bool {
if self.disclosed_atps.is_empty() {
return true;
}
let consented_set: HashSet<u16> = self.consented_atps.iter().copied().collect();
self.disclosed_atps
.iter()
.all(|&atp| consented_set.contains(&atp))
}
}
impl Default for ACString {
fn default() -> Self {
Self::new()
}
}
#[must_use = "AC string parsing failure must be handled"]
pub fn parse_ac_string(ac_string: &str) -> Result<ACString, ACError> {
if ac_string.is_empty() {
return Err(ACError::Empty);
}
let parts: Vec<&str> = ac_string.split('~').collect();
if parts.is_empty() {
return Err(ACError::InvalidFormat("No parts found"));
}
let version = parts[0]
.parse::<u8>()
.map_err(|_| ACError::InvalidVersion(parts[0].to_string()))?;
if version != 1 && version != 2 {
return Err(ACError::UnsupportedVersion(version));
}
let consented_atps = if parts.len() > 1 && !parts[1].is_empty() {
parse_atp_list(parts[1])?
} else {
Vec::new()
};
let disclosed_atps = if version == 2 && parts.len() > 2 && !parts[2].is_empty() {
if !parts[2].starts_with("dv.") {
return Err(ACError::InvalidDisclosedPrefix);
}
parse_atp_list(&parts[2][3..])?
} else {
Vec::new()
};
Ok(ACString {
version,
consented_atps,
disclosed_atps,
})
}
#[must_use]
pub fn encode_ac_string(ac: &ACString) -> String {
let mut parts = vec![ac.version.to_string()];
if ac.consented_atps.is_empty() {
parts.push(String::new());
} else {
parts.push(encode_atp_list(&ac.consented_atps));
}
if ac.version == 2 && !ac.disclosed_atps.is_empty() {
parts.push(format!("dv.{}", encode_atp_list(&ac.disclosed_atps)));
}
parts.join("~")
}
fn parse_atp_list(list: &str) -> Result<Vec<u16>, ACError> {
list.split('.')
.map(|s| {
s.parse::<u16>()
.map_err(|_| ACError::InvalidAtpId(s.to_string()))
})
.collect()
}
fn encode_atp_list(atps: &[u16]) -> String {
atps.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(".")
}
pub fn validate_ac_string_atps<S: std::hash::BuildHasher>(
ac: &ACString,
valid_atp_ids: &HashSet<u16, S>,
) -> Result<(), ACError> {
for &atp_id in &ac.consented_atps {
if !valid_atp_ids.contains(&atp_id) {
return Err(ACError::UnknownAtpId(atp_id));
}
}
for &atp_id in &ac.disclosed_atps {
if !valid_atp_ids.contains(&atp_id) {
return Err(ACError::UnknownAtpId(atp_id));
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ACError {
#[error("AC String is empty")]
Empty,
#[error("Invalid AC String format: {0}")]
InvalidFormat(&'static str),
#[error("Invalid version: {0}")]
InvalidVersion(String),
#[error("Unsupported AC String version: {0}")]
UnsupportedVersion(u8),
#[error("Disclosed ATPs must start with 'dv.' prefix")]
InvalidDisclosedPrefix,
#[error("Invalid ATP ID: {0}")]
InvalidAtpId(String),
#[error("Unknown ATP ID: {0} (not in official Google ATP List)")]
UnknownAtpId(u16),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ac_string_new() {
let ac = ACString::new();
assert_eq!(ac.version, 2);
assert!(ac.consented_atps.is_empty());
assert!(ac.disclosed_atps.is_empty());
}
#[test]
fn test_ac_string_with_atps() {
let ac = ACString::with_atps(vec![1, 35], vec![41, 101]);
assert_eq!(ac.version, 2);
assert_eq!(ac.consented_atps, vec![1, 35]);
assert_eq!(ac.disclosed_atps, vec![41, 101]);
}
#[test]
fn test_has_consent() {
let ac = ACString::with_atps(vec![1, 35, 41], vec![101]);
assert!(ac.has_consent(1));
assert!(ac.has_consent(35));
assert!(ac.has_consent(41));
assert!(!ac.has_consent(101));
assert!(!ac.has_consent(200));
}
#[test]
fn test_is_disclosed() {
let ac = ACString::with_atps(vec![1, 35], vec![41, 101]);
assert!(ac.is_disclosed(1)); assert!(ac.is_disclosed(35)); assert!(ac.is_disclosed(41)); assert!(ac.is_disclosed(101)); assert!(!ac.is_disclosed(200)); }
#[test]
fn test_all_atps() {
let ac = ACString::with_atps(vec![1, 35], vec![41, 101, 1]); assert_eq!(ac.all_atps(), vec![1, 35, 41, 101]);
}
#[test]
fn test_counts() {
let ac = ACString::with_atps(vec![1, 35, 41], vec![101, 200]);
assert_eq!(ac.consented_count(), 3);
assert_eq!(ac.disclosed_count(), 2);
assert_eq!(ac.total_count(), 5);
}
#[test]
fn test_is_empty() {
let empty = ACString::new();
assert!(empty.is_empty());
let not_empty = ACString::with_atps(vec![1], vec![]);
assert!(!not_empty.is_empty());
}
#[test]
fn test_is_legacy() {
let mut v1 = ACString::new();
v1.version = 1;
assert!(v1.is_legacy());
let v2 = ACString::new();
assert!(!v2.is_legacy());
}
#[test]
fn test_parse_v2_full() {
let result = parse_ac_string("2~1.35.41.101~dv.9.21.81").unwrap();
assert_eq!(result.version, 2);
assert_eq!(result.consented_atps, vec![1, 35, 41, 101]);
assert_eq!(result.disclosed_atps, vec![9, 21, 81]);
}
#[test]
fn test_parse_v2_no_consented() {
let result = parse_ac_string("2~~dv.1.35.41.101").unwrap();
assert_eq!(result.version, 2);
assert!(result.consented_atps.is_empty());
assert_eq!(result.disclosed_atps, vec![1, 35, 41, 101]);
}
#[test]
fn test_parse_v2_no_disclosed() {
let result = parse_ac_string("2~1.35.41.101").unwrap();
assert_eq!(result.version, 2);
assert_eq!(result.consented_atps, vec![1, 35, 41, 101]);
assert!(result.disclosed_atps.is_empty());
}
#[test]
fn test_parse_v2_empty_parts() {
let result = parse_ac_string("2~~").unwrap();
assert_eq!(result.version, 2);
assert!(result.consented_atps.is_empty());
assert!(result.disclosed_atps.is_empty());
}
#[test]
fn test_parse_v1_full() {
let result = parse_ac_string("1~1.35.41.101").unwrap();
assert_eq!(result.version, 1);
assert_eq!(result.consented_atps, vec![1, 35, 41, 101]);
assert!(result.disclosed_atps.is_empty());
}
#[test]
fn test_parse_v1_empty_consented() {
let result = parse_ac_string("1~").unwrap();
assert_eq!(result.version, 1);
assert!(result.consented_atps.is_empty());
assert!(result.disclosed_atps.is_empty());
}
#[test]
fn test_parse_empty_string() {
let result = parse_ac_string("");
assert!(matches!(result, Err(ACError::Empty)));
}
#[test]
fn test_parse_invalid_version() {
let result = parse_ac_string("x~1.35");
assert!(matches!(result, Err(ACError::InvalidVersion(_))));
}
#[test]
fn test_parse_unsupported_version() {
let result = parse_ac_string("3~1.35");
assert!(matches!(result, Err(ACError::UnsupportedVersion(3))));
}
#[test]
fn test_parse_invalid_disclosed_prefix() {
let result = parse_ac_string("2~1.35~41.101");
assert!(matches!(result, Err(ACError::InvalidDisclosedPrefix)));
}
#[test]
fn test_parse_invalid_atp_id() {
let result = parse_ac_string("2~1.abc.35");
assert!(matches!(result, Err(ACError::InvalidAtpId(_))));
}
#[test]
fn test_encode_v2_full() {
let ac = ACString::with_atps(vec![1, 35, 41], vec![9, 21]);
assert_eq!(encode_ac_string(&ac), "2~1.35.41~dv.9.21");
}
#[test]
fn test_encode_v2_no_consented() {
let ac = ACString {
version: 2,
consented_atps: vec![],
disclosed_atps: vec![1, 35],
};
assert_eq!(encode_ac_string(&ac), "2~~dv.1.35");
}
#[test]
fn test_encode_v2_no_disclosed() {
let ac = ACString::with_atps(vec![1, 35], vec![]);
assert_eq!(encode_ac_string(&ac), "2~1.35");
}
#[test]
fn test_encode_v1() {
let ac = ACString {
version: 1,
consented_atps: vec![1, 35, 41],
disclosed_atps: vec![], };
assert_eq!(encode_ac_string(&ac), "1~1.35.41");
}
#[test]
fn test_roundtrip_v2_full() {
let original = "2~1.35.41.101~dv.9.21.81";
let parsed = parse_ac_string(original).unwrap();
let encoded = encode_ac_string(&parsed);
let reparsed = parse_ac_string(&encoded).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn test_roundtrip_v1() {
let original = "1~1.35.41.101";
let parsed = parse_ac_string(original).unwrap();
let encoded = encode_ac_string(&parsed);
let reparsed = parse_ac_string(&encoded).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn test_validate_valid_atps() {
let ac = ACString::with_atps(vec![1, 35], vec![41]);
let valid_ids: HashSet<u16> = vec![1, 35, 41, 101, 200].into_iter().collect();
assert!(validate_ac_string_atps(&ac, &valid_ids).is_ok());
}
#[test]
fn test_validate_unknown_consented_atp() {
let ac = ACString::with_atps(vec![1, 999], vec![]);
let valid_ids: HashSet<u16> = vec![1, 35, 41].into_iter().collect();
let result = validate_ac_string_atps(&ac, &valid_ids);
assert!(matches!(result, Err(ACError::UnknownAtpId(999))));
}
#[test]
fn test_validate_unknown_disclosed_atp() {
let ac = ACString::with_atps(vec![1], vec![999]);
let valid_ids: HashSet<u16> = vec![1, 35, 41].into_iter().collect();
let result = validate_ac_string_atps(&ac, &valid_ids);
assert!(matches!(result, Err(ACError::UnknownAtpId(999))));
}
}