[channel] Move channel.rs to its own crate

This is necessary for use in both bin/omaha-client and build scripts.

Bug: 103263
Change-Id: I593f57bf2e24fbcd9b3abd7ee2015ed4ba6e9638
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/693782
Reviewed-by: Sen Jiang <senj@google.com>
Commit-Queue: James Buckland <jbuckland@google.com>
diff --git a/src/sys/pkg/bin/omaha-client/BUILD.gn b/src/sys/pkg/bin/omaha-client/BUILD.gn
index 0610a00..e9daa40 100644
--- a/src/sys/pkg/bin/omaha-client/BUILD.gn
+++ b/src/sys/pkg/bin/omaha-client/BUILD.gn
@@ -53,6 +53,7 @@
     "//src/sys/lib/fidl-connector",
     "//src/sys/pkg/fidl/fuchsia.update.installer:fuchsia.update.installer-rustc",
     "//src/sys/pkg/lib/bounded-node",
+    "//src/sys/pkg/lib/channel-config",
     "//src/sys/pkg/lib/event-queue",
     "//src/sys/pkg/lib/fidl-fuchsia-update-ext",
     "//src/sys/pkg/lib/fidl-fuchsia-update-installer-ext",
diff --git a/src/sys/pkg/bin/omaha-client/src/app_set.rs b/src/sys/pkg/bin/omaha-client/src/app_set.rs
index 8a5b1dd8..b651ee5 100644
--- a/src/sys/pkg/bin/omaha-client/src/app_set.rs
+++ b/src/sys/pkg/bin/omaha-client/src/app_set.rs
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 use {
-    crate::channel::ChannelConfigs,
+    channel_config::ChannelConfigs,
     omaha_client::{app_set::AppSet, common::App},
 };
 
diff --git a/src/sys/pkg/bin/omaha-client/src/channel.rs b/src/sys/pkg/bin/omaha-client/src/channel.rs
index b1cb793..9cd162c 100644
--- a/src/sys/pkg/bin/omaha-client/src/channel.rs
+++ b/src/sys/pkg/bin/omaha-client/src/channel.rs
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use omaha_client::protocol::Cohort;
-use serde::{Deserialize, Serialize};
+use channel_config::ChannelConfigs;
+use serde::Deserialize;
 use std::fs;
 use std::io;
 
@@ -20,101 +20,6 @@
     Version1(ChannelConfigs),
 }
 
-/// Wrapper for deserializing repository configs to the on-disk JSON format.
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
-pub struct ChannelConfigs {
-    pub default_channel: Option<String>,
-    #[serde(rename = "channels")]
-    pub known_channels: Vec<ChannelConfig>,
-}
-
-impl ChannelConfigs {
-    pub fn validate(&self) -> Result<(), io::Error> {
-        let names: Vec<&str> = self.known_channels.iter().map(|c| c.name.as_str()).collect();
-        if !names.iter().all(|n| Cohort::validate_name(n)) {
-            return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid channel name"));
-        }
-        if let Some(default) = &self.default_channel {
-            if !names.contains(&default.as_str()) {
-                return Err(io::Error::new(
-                    io::ErrorKind::InvalidData,
-                    "default channel not a known channel",
-                ));
-            }
-        }
-        Ok(())
-    }
-
-    pub fn get_default_channel(&self) -> Option<ChannelConfig> {
-        self.default_channel.as_ref().and_then(|default| self.get_channel(&default))
-    }
-
-    pub fn get_channel(&self, name: &str) -> Option<ChannelConfig> {
-        self.known_channels
-            .iter()
-            .find(|channel_config| channel_config.name == name)
-            .map(|c| c.clone())
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
-pub struct ChannelConfig {
-    pub name: String,
-    pub repo: String,
-    pub appid: Option<String>,
-    pub check_interval_secs: Option<u64>,
-}
-
-#[cfg(test)]
-impl ChannelConfig {
-    pub fn new(name: &str) -> Self {
-        ChannelConfigBuilder::new(name, name.to_owned() + "-repo").build()
-    }
-
-    pub fn with_appid(name: &str, appid: &str) -> Self {
-        ChannelConfigBuilder::new(name, name.to_owned() + "-repo").appid(appid).build()
-    }
-}
-
-#[cfg(test)]
-#[derive(Debug, Default)]
-pub struct ChannelConfigBuilder {
-    name: String,
-    repo: String,
-    appid: Option<String>,
-    check_interval_secs: Option<u64>,
-}
-
-#[cfg(test)]
-impl ChannelConfigBuilder {
-    pub fn new(name: impl Into<String>, repo: impl Into<String>) -> Self {
-        ChannelConfigBuilder {
-            name: name.into(),
-            repo: repo.into(),
-            ..ChannelConfigBuilder::default()
-        }
-    }
-
-    pub fn appid(mut self, appid: impl Into<String>) -> Self {
-        self.appid = Some(appid.into());
-        self
-    }
-
-    pub fn check_interval_secs(mut self, check_interval_secs: u64) -> Self {
-        self.check_interval_secs = Some(check_interval_secs);
-        self
-    }
-
-    pub fn build(self) -> ChannelConfig {
-        ChannelConfig {
-            name: self.name,
-            repo: self.repo,
-            appid: self.appid,
-            check_interval_secs: self.check_interval_secs,
-        }
-    }
-}
-
 /// This method retrieves the channel configuation from the file in config data
 pub fn get_configs() -> Result<ChannelConfigs, io::Error> {
     get_configs_from_path(CHANNEL_CONFIG_PATH)
@@ -214,77 +119,8 @@
     }
 
     #[test]
-    fn test_channel_configs_get_default() {
-        let configs = ChannelConfigs {
-            default_channel: Some("default_channel".to_string()),
-            known_channels: vec![
-                ChannelConfig::new("some_channel"),
-                ChannelConfig::new("default_channel"),
-                ChannelConfig::new("other"),
-            ],
-        };
-        assert_eq!(configs.get_default_channel().unwrap(), configs.known_channels[1]);
-    }
-
-    #[test]
-    fn test_channel_configs_get_default_none() {
-        let configs = ChannelConfigs {
-            default_channel: None,
-            known_channels: vec![ChannelConfig::new("some_channel")],
-        };
-        assert_eq!(configs.get_default_channel(), None);
-    }
-
-    #[test]
-    fn test_channel_configs_get_channel() {
-        let configs = ChannelConfigs {
-            default_channel: Some("default_channel".to_string()),
-            known_channels: vec![
-                ChannelConfig::new("some_channel"),
-                ChannelConfig::new("default_channel"),
-                ChannelConfig::new("other"),
-            ],
-        };
-        assert_eq!(configs.get_channel("other").unwrap(), configs.known_channels[2]);
-    }
-
-    #[test]
-    fn test_channel_configs_get_channel_missing() {
-        let configs = ChannelConfigs {
-            default_channel: Some("default_channel".to_string()),
-            known_channels: vec![
-                ChannelConfig::new("some_channel"),
-                ChannelConfig::new("default_channel"),
-                ChannelConfig::new("other"),
-            ],
-        };
-        assert_eq!(configs.get_channel("missing"), None);
-    }
-
-    #[test]
     fn test_missing_channel_configs_file_behavior() {
         let config = get_configs_from_path("/config/data/invalid.path");
         assert!(config.is_err());
     }
-
-    #[test]
-    fn test_channel_cfg_builder_app_id() {
-        let config = ChannelConfigBuilder::new("name", "repo").appid("appid").build();
-        assert_eq!("name", config.name);
-        assert_eq!("repo", config.repo);
-        assert_eq!(Some("appid".to_owned()), config.appid);
-        assert_eq!(None, config.check_interval_secs);
-    }
-
-    #[test]
-    fn test_channel_cfg_builder_check_interval() {
-        let config = ChannelConfigBuilder::new("name", "repo")
-            .appid("appid")
-            .check_interval_secs(3600)
-            .build();
-        assert_eq!("name", config.name);
-        assert_eq!("repo", config.repo);
-        assert_eq!(Some("appid".to_owned()), config.appid);
-        assert_eq!(Some(3600), config.check_interval_secs);
-    }
 }
diff --git a/src/sys/pkg/bin/omaha-client/src/configuration.rs b/src/sys/pkg/bin/omaha-client/src/configuration.rs
index 66fc5cb..a0fe8fe 100644
--- a/src/sys/pkg/bin/omaha-client/src/configuration.rs
+++ b/src/sys/pkg/bin/omaha-client/src/configuration.rs
@@ -4,10 +4,10 @@
 
 use crate::{
     app_set::{EagerPackage, FuchsiaAppSet},
-    channel::{ChannelConfig, ChannelConfigs},
     eager_package_config::{EagerPackageConfig, EagerPackageConfigs},
 };
 use anyhow::{anyhow, Error};
+use channel_config::{ChannelConfig, ChannelConfigs};
 use fidl_fuchsia_boot::{ArgumentsMarker, ArgumentsProxy};
 use fidl_fuchsia_pkg::{self as fpkg, CupMarker, CupProxy, GetInfoError};
 use log::{error, info, warn};
@@ -389,7 +389,7 @@
 
     #[fasync::run_singlethreaded(test)]
     async fn test_channel_data_configured() {
-        let channel_config = ChannelConfig::with_appid("some-channel", "some-appid");
+        let channel_config = ChannelConfig::with_appid_for_test("some-channel", "some-appid");
         let channel_configs = ChannelConfigs {
             default_channel: Some(channel_config.name.clone()),
             known_channels: vec![channel_config.clone()],
@@ -415,10 +415,10 @@
             Some(&ChannelConfigs {
                 default_channel: Some("some-channel".to_string()),
                 known_channels: vec![
-                    ChannelConfig::new("no-appid-channel"),
-                    ChannelConfig::with_appid("wrong-channel", "wrong-appid"),
-                    ChannelConfig::with_appid("some-channel", "some-appid"),
-                    ChannelConfig::with_appid("some-other-channel", "some-other-appid"),
+                    ChannelConfig::new_for_test("no-appid-channel"),
+                    ChannelConfig::with_appid_for_test("wrong-channel", "wrong-appid"),
+                    ChannelConfig::with_appid_for_test("some-channel", "some-appid"),
+                    ChannelConfig::with_appid_for_test("some-other-channel", "some-other-appid"),
                 ],
             }),
             VbMetaData::default(),
@@ -448,7 +448,10 @@
             "1.2.3.4",
             Some(&ChannelConfigs {
                 default_channel: Some("wrong-channel".to_string()),
-                known_channels: vec![ChannelConfig::with_appid("wrong-channel", "wrong-appid")],
+                known_channels: vec![ChannelConfig::with_appid_for_test(
+                    "wrong-channel",
+                    "wrong-appid",
+                )],
             }),
             VbMetaData {
                 appid: Some("vbmeta-appid".to_string()),
diff --git a/src/sys/pkg/bin/omaha-client/src/eager_package_config.rs b/src/sys/pkg/bin/omaha-client/src/eager_package_config.rs
index 19dfb08..a14cb58 100644
--- a/src/sys/pkg/bin/omaha-client/src/eager_package_config.rs
+++ b/src/sys/pkg/bin/omaha-client/src/eager_package_config.rs
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::channel::ChannelConfigs;
 use anyhow::{Context as _, Error};
+use channel_config::ChannelConfigs;
 use fuchsia_url::UnpinnedAbsolutePackageUrl;
 use omaha_client::cup_ecdsa::PublicKeys;
 use serde::Deserialize;
@@ -72,8 +72,8 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::channel::ChannelConfig;
     use assert_matches::assert_matches;
+    use channel_config::ChannelConfig;
     use omaha_client::cup_ecdsa::{
         test_support::{
             make_default_json_public_keys_for_test, make_default_public_key_for_test,
diff --git a/src/sys/pkg/bin/omaha-client/src/fidl.rs b/src/sys/pkg/bin/omaha-client/src/fidl.rs
index ac68bba..5d6c019 100644
--- a/src/sys/pkg/bin/omaha-client/src/fidl.rs
+++ b/src/sys/pkg/bin/omaha-client/src/fidl.rs
@@ -5,10 +5,10 @@
 use crate::{
     api_metrics::{ApiEvent, ApiMetricsReporter},
     app_set::FuchsiaAppSet,
-    channel::ChannelConfigs,
     inspect::{AppsNode, StateNode},
 };
 use anyhow::{anyhow, Context as _, Error};
+use channel_config::ChannelConfigs;
 use event_queue::{ClosedClient, ControlHandle, Event, EventQueue, Notify};
 use fidl::endpoints::ClientEnd;
 use fidl_fuchsia_hardware_power_statecontrol::RebootReason;
@@ -962,8 +962,8 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::channel::ChannelConfig;
     use assert_matches::assert_matches;
+    use channel_config::ChannelConfig;
     use fidl::endpoints::{create_proxy_and_stream, create_request_stream};
     use fidl_fuchsia_update::{
         self as update, AttemptsMonitorRequest, ManagerMarker, MonitorMarker, MonitorRequest,
@@ -1371,8 +1371,8 @@
             .with_channel_configs(ChannelConfigs {
                 default_channel: None,
                 known_channels: vec![
-                    ChannelConfig::new("some-channel"),
-                    ChannelConfig::with_appid("target-channel", "target-id"),
+                    ChannelConfig::new_for_test("some-channel"),
+                    ChannelConfig::with_appid_for_test("target-channel", "target-id"),
                 ],
             })
             .build()
@@ -1399,7 +1399,10 @@
         let fidl = FidlServerBuilder::new()
             .with_channel_configs(ChannelConfigs {
                 default_channel: "default-channel".to_string().into(),
-                known_channels: vec![ChannelConfig::with_appid("default-channel", "default-app")],
+                known_channels: vec![ChannelConfig::with_appid_for_test(
+                    "default-channel",
+                    "default-app",
+                )],
             })
             .build()
             .await;
@@ -1452,8 +1455,8 @@
             .with_channel_configs(ChannelConfigs {
                 default_channel: None,
                 known_channels: vec![
-                    ChannelConfig::new("some-channel"),
-                    ChannelConfig::new("some-other-channel"),
+                    ChannelConfig::new_for_test("some-channel"),
+                    ChannelConfig::new_for_test("some-other-channel"),
                 ],
             })
             .build()
@@ -1508,7 +1511,7 @@
             .with_apps_node(apps_node)
             .with_channel_configs(ChannelConfigs {
                 default_channel: None,
-                known_channels: vec![ChannelConfig::new("target-channel")],
+                known_channels: vec![ChannelConfig::new_for_test("target-channel")],
             })
             .build()
             .await;
diff --git a/src/sys/pkg/lib/BUILD.gn b/src/sys/pkg/lib/BUILD.gn
index 1a35e98..392f642 100644
--- a/src/sys/pkg/lib/BUILD.gn
+++ b/src/sys/pkg/lib/BUILD.gn
@@ -8,6 +8,7 @@
     "async-generator",
     "blobfs",
     "bounded-node",
+    "channel-config",
     "epoch",
     "event-queue",
     "far:lib",
@@ -36,6 +37,7 @@
     "async-generator:tests",
     "blobfs:tests",
     "bounded-node:tests",
+    "channel-config:tests",
     "epoch:tests",
     "event-queue:tests",
     "far:tests",
diff --git a/src/sys/pkg/lib/channel-config/BUILD.gn b/src/sys/pkg/lib/channel-config/BUILD.gn
new file mode 100644
index 0000000..32d7981
--- /dev/null
+++ b/src/sys/pkg/lib/channel-config/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2022 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.
+
+import("//build/components.gni")
+import("//build/config.gni")
+import("//build/rust/rustc_library.gni")
+
+rustc_library("channel-config") {
+  version = "0.0.1"
+  edition = "2021"
+  with_unit_tests = true
+
+  deps = [
+    "//src/sys/pkg/lib/omaha-client",
+    "//src/sys/pkg/lib/version",
+    "//third_party/rust_crates:serde",
+    "//third_party/rust_crates:serde_json",
+  ]
+
+  test_deps = [ "//third_party/rust_crates:pretty_assertions" ]
+
+  sources = [ "src/lib.rs" ]
+}
+
+fuchsia_unittest_package("channel-config-lib-tests") {
+  deps = [ ":channel-config_test" ]
+}
+
+group("tests") {
+  testonly = true
+  deps = [ ":channel-config-lib-tests" ]
+}
diff --git a/src/sys/pkg/lib/channel-config/OWNERS b/src/sys/pkg/lib/channel-config/OWNERS
new file mode 100644
index 0000000..f29ac69
--- /dev/null
+++ b/src/sys/pkg/lib/channel-config/OWNERS
@@ -0,0 +1,4 @@
+senj@google.com
+jbuckland@google.com
+
+# COMPONENT: Packages>OMCL
diff --git a/src/sys/pkg/lib/channel-config/README.md b/src/sys/pkg/lib/channel-config/README.md
new file mode 100644
index 0000000..02295f7
--- /dev/null
+++ b/src/sys/pkg/lib/channel-config/README.md
@@ -0,0 +1,6 @@
+# Channel library
+
+Updated: 2022-06
+
+This crate contains the implementation for reading the `channel_config.json`
+configuration file.
diff --git a/src/sys/pkg/lib/channel-config/src/lib.rs b/src/sys/pkg/lib/channel-config/src/lib.rs
new file mode 100644
index 0000000..22b2029
--- /dev/null
+++ b/src/sys/pkg/lib/channel-config/src/lib.rs
@@ -0,0 +1,177 @@
+// Copyright 2022 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 omaha_client::protocol::Cohort;
+use serde::{Deserialize, Serialize};
+use std::io;
+
+/// Wrapper for deserializing repository configs to the on-disk JSON format.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
+pub struct ChannelConfigs {
+    pub default_channel: Option<String>,
+    #[serde(rename = "channels")]
+    pub known_channels: Vec<ChannelConfig>,
+}
+
+impl ChannelConfigs {
+    pub fn validate(&self) -> Result<(), io::Error> {
+        let names: Vec<&str> = self.known_channels.iter().map(|c| c.name.as_str()).collect();
+        if !names.iter().all(|n| Cohort::validate_name(n)) {
+            return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid channel name"));
+        }
+        if let Some(default) = &self.default_channel {
+            if !names.contains(&default.as_str()) {
+                return Err(io::Error::new(
+                    io::ErrorKind::InvalidData,
+                    "default channel not a known channel",
+                ));
+            }
+        }
+        Ok(())
+    }
+
+    pub fn get_default_channel(&self) -> Option<ChannelConfig> {
+        self.default_channel.as_ref().and_then(|default| self.get_channel(&default))
+    }
+
+    pub fn get_channel(&self, name: &str) -> Option<ChannelConfig> {
+        self.known_channels
+            .iter()
+            .find(|channel_config| channel_config.name == name)
+            .map(|c| c.clone())
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub struct ChannelConfig {
+    pub name: String,
+    pub repo: String,
+    pub appid: Option<String>,
+    pub check_interval_secs: Option<u64>,
+}
+
+impl ChannelConfig {
+    pub fn new_for_test(name: &str) -> Self {
+        testing::ChannelConfigBuilder::new(name, name.to_owned() + "-repo").build()
+    }
+
+    pub fn with_appid_for_test(name: &str, appid: &str) -> Self {
+        testing::ChannelConfigBuilder::new(name, name.to_owned() + "-repo").appid(appid).build()
+    }
+}
+
+pub mod testing {
+    use super::*;
+    #[derive(Debug, Default)]
+    pub struct ChannelConfigBuilder {
+        name: String,
+        repo: String,
+        appid: Option<String>,
+        check_interval_secs: Option<u64>,
+    }
+
+    impl ChannelConfigBuilder {
+        pub fn new(name: impl Into<String>, repo: impl Into<String>) -> Self {
+            ChannelConfigBuilder {
+                name: name.into(),
+                repo: repo.into(),
+                ..ChannelConfigBuilder::default()
+            }
+        }
+
+        pub fn appid(mut self, appid: impl Into<String>) -> Self {
+            self.appid = Some(appid.into());
+            self
+        }
+
+        pub fn check_interval_secs(mut self, check_interval_secs: u64) -> Self {
+            self.check_interval_secs = Some(check_interval_secs);
+            self
+        }
+
+        pub fn build(self) -> ChannelConfig {
+            ChannelConfig {
+                name: self.name,
+                repo: self.repo,
+                appid: self.appid,
+                check_interval_secs: self.check_interval_secs,
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_channel_configs_get_default() {
+        let configs = ChannelConfigs {
+            default_channel: Some("default_channel".to_string()),
+            known_channels: vec![
+                ChannelConfig::new_for_test("some_channel"),
+                ChannelConfig::new_for_test("default_channel"),
+                ChannelConfig::new_for_test("other"),
+            ],
+        };
+        assert_eq!(configs.get_default_channel().unwrap(), configs.known_channels[1]);
+    }
+
+    #[test]
+    fn test_channel_configs_get_default_none() {
+        let configs = ChannelConfigs {
+            default_channel: None,
+            known_channels: vec![ChannelConfig::new_for_test("some_channel")],
+        };
+        assert_eq!(configs.get_default_channel(), None);
+    }
+
+    #[test]
+    fn test_channel_configs_get_channel() {
+        let configs = ChannelConfigs {
+            default_channel: Some("default_channel".to_string()),
+            known_channels: vec![
+                ChannelConfig::new_for_test("some_channel"),
+                ChannelConfig::new_for_test("default_channel"),
+                ChannelConfig::new_for_test("other"),
+            ],
+        };
+        assert_eq!(configs.get_channel("other").unwrap(), configs.known_channels[2]);
+    }
+
+    #[test]
+    fn test_channel_configs_get_channel_missing() {
+        let configs = ChannelConfigs {
+            default_channel: Some("default_channel".to_string()),
+            known_channels: vec![
+                ChannelConfig::new_for_test("some_channel"),
+                ChannelConfig::new_for_test("default_channel"),
+                ChannelConfig::new_for_test("other"),
+            ],
+        };
+        assert_eq!(configs.get_channel("missing"), None);
+    }
+
+    #[test]
+    fn test_channel_cfg_builder_app_id() {
+        let config = testing::ChannelConfigBuilder::new("name", "repo").appid("appid").build();
+        assert_eq!("name", config.name);
+        assert_eq!("repo", config.repo);
+        assert_eq!(Some("appid".to_owned()), config.appid);
+        assert_eq!(None, config.check_interval_secs);
+    }
+
+    #[test]
+    fn test_channel_cfg_builder_check_interval() {
+        let config = testing::ChannelConfigBuilder::new("name", "repo")
+            .appid("appid")
+            .check_interval_secs(3600)
+            .build();
+        assert_eq!("name", config.name);
+        assert_eq!("repo", config.repo);
+        assert_eq!(Some("appid".to_owned()), config.appid);
+        assert_eq!(Some(3600), config.check_interval_secs);
+    }
+}