Merge "Implement initial hostapd set_ssid()" into main
diff --git a/rust/hostapd-rs/src/hostapd.rs b/rust/hostapd-rs/src/hostapd.rs
index 72a5e06..6a96d77 100644
--- a/rust/hostapd-rs/src/hostapd.rs
+++ b/rust/hostapd-rs/src/hostapd.rs
@@ -19,8 +19,9 @@
 ///
 /// hostapd.conf file is generated under discovery directory.
 ///
+use anyhow::bail;
 use bytes::Bytes;
-use log::warn;
+use log::{info, warn};
 use std::collections::HashMap;
 use std::ffi::{c_char, c_int, CStr, CString};
 use std::fs::File;
@@ -42,7 +43,8 @@
 use tokio::sync::Mutex;
 
 use crate::hostapd_sys::{
-    run_hostapd_main, set_virtio_ctrl_sock, set_virtio_sock, VIRTIO_WIFI_CTRL_CMD_TERMINATE,
+    run_hostapd_main, set_virtio_ctrl_sock, set_virtio_sock, VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG,
+    VIRTIO_WIFI_CTRL_CMD_TERMINATE,
 };
 
 /// Alias for RawFd on Unix or RawSocket on Windows (converted to i32)
@@ -65,7 +67,7 @@
 impl Hostapd {
     pub fn new(tx_bytes: mpsc::Sender<Bytes>, verbose: bool, config_path: PathBuf) -> Self {
         // Default Hostapd conf entries
-        let config_data = vec![
+        let config_data = [
             ("ssid", "AndroidWifi"),
             ("interface", "wlan1"),
             ("driver", "virtio_wifi"),
@@ -86,7 +88,7 @@
             ("eapol_key_index_workaround", "0"),
         ];
         let mut config: HashMap<String, String> = HashMap::new();
-        config.extend(config_data.into_iter().map(|(k, v)| (k.to_string(), v.to_string())));
+        config.extend(config_data.iter().map(|(k, v)| (k.to_string(), v.to_string())));
 
         Hostapd {
             handle: RwLock::new(None),
@@ -143,12 +145,53 @@
         true
     }
 
-    pub fn set_ssid(&mut self, _ssid: String, _password: String) -> bool {
-        todo!();
+    /// Reconfigure Hostapd with specified SSID (and password) config
+    ///
+    /// TODO:
+    /// * implement password & encryption support
+    /// * update as async fn.
+    pub fn set_ssid(&mut self, ssid: String, password: String) -> anyhow::Result<()> {
+        if ssid.is_empty() {
+            bail!("set_ssid must have a non-empty SSID");
+        }
+
+        if !password.is_empty() {
+            bail!("set_ssid with password is not yet supported.");
+        }
+
+        if ssid == self.get_ssid() && password == self.get_config_val("password") {
+            info!("SSID and password matches current configuration.");
+            return Ok(());
+        }
+
+        // Update the config
+        self.config.insert("ssid".to_string(), ssid);
+        if !password.is_empty() {
+            let password_config = [
+                ("wpa", "2"),
+                ("wpa_key_mgmt", "WPA-PSK"),
+                ("rsn_pairwise", "CCMP"),
+                ("wpa_passphrase", &password),
+            ];
+            self.config.extend(password_config.iter().map(|(k, v)| (k.to_string(), v.to_string())));
+        }
+
+        // Update the config file.
+        self.gen_config_file()?;
+
+        // Send command for Hostapd to reload config file
+        if let Err(e) = get_runtime().block_on(Self::async_write(
+            self.ctrl_writer.as_ref().unwrap(),
+            c_string_to_bytes(VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG),
+        )) {
+            bail!("Failed to send VIRTIO_WIFI_CTRL_CMD_RELOAD_CONFIG to hostapd to reload config: {:?}", e);
+        }
+
+        Ok(())
     }
 
-    pub fn get_ssid(&self) -> Option<String> {
-        self.config.get("ssid").cloned()
+    pub fn get_ssid(&self) -> String {
+        self.get_config_val("ssid")
     }
 
     /// Input data packet bytes from netsim to hostapd
@@ -194,6 +237,13 @@
         Ok(writer.flush()?) // Ensure all data is written to the file
     }
 
+    /// Get the value of the given key in the config
+    ///
+    /// Returns empty String if key is not found
+    fn get_config_val(&self, key: &str) -> String {
+        self.config.get(key).cloned().unwrap_or_default()
+    }
+
     /// Creates a pipe of two connected TcpStream objects
     ///
     /// Extracts the first stream's raw descriptor and splits the second stream as OwnedReadHalf and OwnedWriteHalf
@@ -285,7 +335,7 @@
                 }
             };
 
-            if let Err(e) = tx_bytes.send(Bytes::from(buf[..size].to_vec())) {
+            if let Err(e) = tx_bytes.send(Bytes::copy_from_slice(&buf[..size])) {
                 warn!("Failed to send hostapd packet response: {:?}", e);
                 break;
             };
diff --git a/rust/hostapd-rs/tests/integration_test.rs b/rust/hostapd-rs/tests/integration_test.rs
index fdf36d5..85eeb9a 100644
--- a/rust/hostapd-rs/tests/integration_test.rs
+++ b/rust/hostapd-rs/tests/integration_test.rs
@@ -21,16 +21,32 @@
     time::{Duration, Instant},
 };
 
-/// Test whether hostapd starts and terminates successfully
-#[test]
-fn test_hostapd_run_and_terminate() {
+/// Helper function to initialize Hostapd for test
+fn init_test_hostapd() -> Hostapd {
     let (tx, _rx): (mpsc::Sender<Bytes>, mpsc::Receiver<Bytes>) = mpsc::channel();
     let config_path = env::temp_dir().join("hostapd.conf");
-    let mut hostapd = Hostapd::new(tx, true, config_path);
+    Hostapd::new(tx, true, config_path)
+}
+
+/// Hostpad integration test
+///
+/// A single test is used here to avoid conflicts when multiple hostapd runs in parallel
+/// TODO: Split up tests once serial_test crate is available
+#[test]
+fn test_hostapd() {
+    test_start_and_terminate();
+    test_set_ssid();
+}
+
+/// Test Hostapd starts and terminates successfully
+fn test_start_and_terminate() {
+    // Test hostapd starts successfully
+    let mut hostapd = init_test_hostapd();
     hostapd.run();
     assert!(hostapd.is_running());
+
+    // Test hostapd terminates successfully
     hostapd.terminate();
-    // Check that hostapd terminates within 30s
     let max_wait_time = Duration::from_secs(30);
     let start_time = Instant::now();
     while start_time.elapsed() < max_wait_time {
@@ -41,3 +57,34 @@
     }
     assert!(!hostapd.is_running());
 }
+
+/// Test various ways to configure Hostapd SSID and password
+fn test_set_ssid() {
+    let mut hostapd = init_test_hostapd();
+    hostapd.run();
+    assert!(hostapd.is_running());
+
+    // Check default ssid is set
+    assert_eq!(hostapd.get_ssid(), "AndroidWifi");
+    let mut test_ssid = String::new();
+    let mut test_password = String::new();
+
+    // Verify set_ssid fails if SSID is empty
+    assert!(hostapd.set_ssid(test_ssid.clone(), test_password.clone()).is_err());
+
+    // Verify set_ssid succeeds if SSID is not empty
+    test_ssid = "TestSsid".to_string();
+    assert!(hostapd.set_ssid(test_ssid.clone(), test_password.clone()).is_ok());
+    // TODO: Enhance test to verify hostapd response packet SSID
+
+    // Verify ssid was set successfully
+    assert_eq!(hostapd.get_ssid(), test_ssid.clone());
+
+    // Verify setting same ssid again succeeds
+    assert!(hostapd.set_ssid(test_ssid.clone(), test_password.clone()).is_ok());
+
+    // Verify set_ssid fails if password is not empty
+    // TODO: Update once password support is implemented
+    test_password = "TestPassword".to_string();
+    assert!(hostapd.set_ssid(test_ssid.clone(), test_password.clone()).is_err());
+}