iab_gpp 0.1.2

IAB GPP Consent String implementation
Documentation
use crate::core::DataRead;
use crate::sections::{IdSet, SectionDecodeError};
use bitstream_io::BitRead;
use iab_gpp_derive::GPPSection;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

// See https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md
#[derive(Debug, Eq, PartialEq, GPPSection)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[gpp(section_version = 1)]
pub struct TcfEuV1 {
    #[gpp(datetime_as_unix_timestamp)]
    pub created: u64,
    #[gpp(datetime_as_unix_timestamp)]
    pub last_updated: u64,
    pub cmp_id: u16,
    pub cmp_version: u16,
    pub consent_screen: u8,
    #[gpp(string(2))]
    pub consent_language: String,
    pub vendor_list_version: u16,
    #[gpp(fixed_bitfield(24))]
    pub purposes_allowed: IdSet,
    #[gpp(parse_with = parse_vendor_consents)]
    pub vendor_consents: IdSet,
}

fn parse_vendor_consents<R: BitRead + ?Sized>(mut r: &mut R) -> Result<IdSet, SectionDecodeError> {
    let max_vendor_id = r.read_unsigned::<16, u16>()?;
    let is_range = r.read_bit()?;
    Ok(if is_range {
        // range section
        let default_consent = r.read_bit()?;
        let ids = BTreeSet::from_iter(r.read_integer_range()?);

        // create final vendor list based on the default consent:
        // only return list of vendors who consent
        (1..=max_vendor_id)
            .filter(|id| {
                //(default_consent && !ids.contains(id)) || (!default_consent && ids.contains(id))
                default_consent ^ ids.contains(id)
            })
            .collect()
    } else {
        // bitfield section
        r.read_fixed_bitfield(max_vendor_id as usize)?
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;
    use test_case::test_case;

    #[test_case("BO5a1L7O5a1L7AAABBENC2-AAAAtH" => matches SectionDecodeError::Read { .. } ; "missing data")]
    #[test_case("" => matches SectionDecodeError::Read { .. } ; "empty string")]
    #[test_case("DOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA" => matches SectionDecodeError::UnknownSegmentVersion { segment_version: 3 } ; "unknown segment version")]
    fn error(s: &str) -> SectionDecodeError {
        TcfEuV1::from_str(s).unwrap_err()
    }
}