omaha_client/
protocol.rs

1// Copyright 2019 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9use serde::{Deserialize, Serialize};
10
11pub mod request;
12pub mod response;
13
14pub const PROTOCOL_V3: &str = "3.0";
15
16/// The cohort identifies the update 'track' or 'channel', and is used to implement the tracking of
17/// membership in a fractional roll-out.  This is per-application data.
18///
19/// This is sent to Omaha to identify the cohort that the application is in.  This is returned (with
20/// possibly new values) by Omaha to indicate that the application is now in a different cohort.  On
21/// the next update check for that application, the updater needs to use this newly returned cohort
22/// as the one that it sends to Omaha with that application.
23///
24/// For more information about cohorts, see the 'cohort', 'cohorthint', and 'cohortname' attributes
25/// of the Request.App object at:
26///
27/// https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#app-request
28#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
29pub struct Cohort {
30    /// This is the cohort id itself.
31    #[serde(rename = "cohort")]
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub id: Option<String>,
34
35    #[serde(rename = "cohorthint")]
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub hint: Option<String>,
38
39    #[serde(rename = "cohortname")]
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub name: Option<String>,
42}
43
44impl Cohort {
45    /// Create a new Cohort instance from just a cohort id (channel name).
46    pub fn new(id: &str) -> Cohort {
47        Cohort {
48            id: Some(id.to_string()),
49            hint: None,
50            name: None,
51        }
52    }
53
54    pub fn from_hint(hint: &str) -> Cohort {
55        Cohort {
56            id: None,
57            hint: Some(hint.to_string()),
58            name: None,
59        }
60    }
61
62    pub fn update_from_omaha(&mut self, omaha_cohort: Self) {
63        // From Omaha spec:
64        // If this attribute is transmitted in the response (even if the value is empty-string),
65        // the client should overwrite the current cohort of this app with the sent value.
66        if omaha_cohort.id.is_some() {
67            self.id = omaha_cohort.id;
68        }
69        if omaha_cohort.hint.is_some() {
70            self.hint = omaha_cohort.hint;
71        }
72        if omaha_cohort.name.is_some() {
73            self.name = omaha_cohort.name;
74        }
75    }
76
77    /// A validation function to test that a given Cohort hint or name is valid per the Omaha spec:
78    ///  1-1024 ascii characters, with values in the range [\u20-\u7e].
79    pub fn validate_name(name: &str) -> bool {
80        !name.is_empty()
81            && name.len() <= 1024
82            && name.chars().all(|c| ('\u{20}'..='\u{7e}').contains(&c))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_cohort_new() {
92        let cohort = Cohort::new("my_cohort");
93        assert_eq!(Some("my_cohort".to_string()), cohort.id);
94        assert_eq!(None, cohort.hint);
95        assert_eq!(None, cohort.name);
96    }
97
98    #[test]
99    fn test_cohort_update_from_omaha() {
100        let mut cohort = Cohort::from_hint("hint");
101        let omaha_cohort = Cohort::new("my_cohort");
102        cohort.update_from_omaha(omaha_cohort);
103        assert_eq!(Some("my_cohort".to_string()), cohort.id);
104        assert_eq!(Some("hint".to_string()), cohort.hint);
105        assert_eq!(None, cohort.name);
106    }
107
108    #[test]
109    fn test_cohort_update_from_omaha_none() {
110        let mut cohort = Cohort {
111            id: Some("id".to_string()),
112            hint: Some("hint".to_string()),
113            name: Some("name".to_string()),
114        };
115        let expected_cohort = cohort.clone();
116        cohort.update_from_omaha(Cohort::default());
117        assert_eq!(cohort, expected_cohort);
118    }
119
120    #[test]
121    fn test_valid_cohort_names() {
122        assert!(Cohort::validate_name("some-channel"));
123        assert!(Cohort::validate_name("a"));
124
125        let max_len_name = "a".repeat(1024);
126        assert!(Cohort::validate_name(&max_len_name));
127    }
128
129    #[test]
130    fn test_invalid_cohort_name_length() {
131        assert!(!Cohort::validate_name(""));
132
133        let too_long_name = "a".repeat(1025);
134        assert!(!Cohort::validate_name(&too_long_name));
135    }
136
137    #[test]
138    fn test_invalid_cohort_name_chars() {
139        assert!(!Cohort::validate_name("some\u{09}channel"));
140        assert!(!Cohort::validate_name("some\u{07f}channel"));
141        assert!(!Cohort::validate_name("some\u{080}channel"));
142    }
143}