Skip to content
135 changes: 134 additions & 1 deletion idevice/src/services/springboardservices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
//! which manages home screen and app icon related operations.

use crate::{Idevice, IdeviceError, IdeviceService, obf};
use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds};

/// Client for interacting with the iOS SpringBoard services
///
Expand Down Expand Up @@ -70,4 +70,137 @@ impl SpringBoardServicesClient {
_ => Err(IdeviceError::UnexpectedResponse),
}
}

/// Retrieves the current icon state from the device
///
/// The icon state contains the layout and organization of all apps on the home screen,
/// including folder structures and icon positions. This is a read-only operation.
///
/// # Arguments
/// * `format_version` - Optional format version string for the icon state format
///
/// # Returns
/// A plist Value containing the complete icon state structure
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let icon_state = client.get_icon_state(None).await?;
/// println!("Icon state: {:?}", icon_state);
/// ```
///
/// # Notes
/// This method successfully reads the home screen layout on all iOS versions.
/// Note that modifying and setting icon state (setIconState) does not work
/// on iOS 18+ due to Apple's security restrictions. See issue #62 for details.
pub async fn get_icon_state(
&mut self,
format_version: Option<String>,
) -> Result<plist::Value, IdeviceError> {
let mut req = crate::plist!({
"command": "getIconState",
});

if let Some(version) = format_version {
if let Some(dict) = req.as_dictionary_mut() {
dict.insert("formatVersion".to_string(), plist::Value::String(version));
}
}

self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist_value().await?;

truncate_dates_to_seconds(&mut res);

Ok(res)
}

/// Sets the icon state on the device
///
/// This method allows you to modify the home screen layout by providing a new icon state.
/// The icon state structure should match the format returned by `get_icon_state`.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let mut icon_state = client.get_icon_state(None).await?;
///
/// // Modify the icon state (e.g., swap two icons)
/// // ... modify icon_state ...
///
/// client.set_icon_state(icon_state).await?;
/// println!("Icon state updated successfully");
/// ```
///
/// # Notes
/// - This method does NOT work on iOS 18+ due to Apple's security restrictions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still necessary?

/// - On supported iOS versions, changes take effect immediately
/// - The device may validate the icon state structure before applying
/// - Invalid icon states will be rejected by the device
pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> {
let req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
});

self.idevice.send_plist(req).await?;
Ok(())
}

/// Sets the icon state with a specific format version
///
/// This is similar to `set_icon_state` but allows specifying a format version.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
/// * `format_version` - Optional format version string
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
pub async fn set_icon_state_with_version(
&mut self,
icon_state: plist::Value,
format_version: Option<String>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, string ref and :? syntax

) -> Result<(), IdeviceError> {
let mut req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
});

if let Some(version) = format_version {
if let Some(dict) = req.as_dictionary_mut() {
dict.insert("formatVersion".to_string(), plist::Value::String(version));
}
}

self.idevice.send_plist(req).await?;
Ok(())
}
}
2 changes: 2 additions & 0 deletions idevice/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

#[cfg(all(feature = "afc", feature = "installation_proxy"))]
pub mod installation;

pub mod plist;
155 changes: 155 additions & 0 deletions idevice/src/utils/plist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/// Utilities for working with plist values

/// Truncates all Date values in a plist structure to second precision.
///
/// This function recursively walks through a plist Value and truncates any Date values
/// from nanosecond precision to second precision. This is necessary for compatibility
/// with iOS devices that reject high-precision date formats.
///
/// # Arguments
/// * `value` - The plist Value to normalize (modified in place)
///
/// # Example
/// ```rust,no_run
/// use idevice::utils::plist::truncate_dates_to_seconds;
/// use plist::Value;
///
/// let mut icon_state = Value::Array(vec![]);
/// truncate_dates_to_seconds(&mut icon_state);
/// ```
///
/// # Details
/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision)
/// - To format: `2026-01-17T03:09:58Z` (second precision)
/// - Recursively processes Arrays and Dictionaries
/// - Other value types are left unchanged
pub fn truncate_dates_to_seconds(value: &mut plist::Value) {
match value {
plist::Value::Date(date) => {
let xml_string = date.to_xml_format();
if let Some(dot_pos) = xml_string.find('.') {
if xml_string[dot_pos..].contains('Z') {
let truncated_string = format!("{}Z", &xml_string[..dot_pos]);
if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) {
*date = new_date;
}
}
}
}
plist::Value::Array(arr) => {
for item in arr.iter_mut() {
truncate_dates_to_seconds(item);
}
}
plist::Value::Dictionary(dict) => {
for (_, v) in dict.iter_mut() {
truncate_dates_to_seconds(v);
}
}
_ => {}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_truncate_date_with_nanoseconds() {
let date_str = "2026-01-17T03:09:58.332738876Z";
let date = plist::Date::from_xml_format(date_str).unwrap();
let mut value = plist::Value::Date(date);

truncate_dates_to_seconds(&mut value);

if let plist::Value::Date(truncated_date) = value {
let result = truncated_date.to_xml_format();
assert!(
!result.contains('.'),
"Date should not contain fractional seconds"
);
assert!(result.ends_with('Z'), "Date should end with Z");
assert!(
result.starts_with("2026-01-17T03:09:58"),
"Date should preserve main timestamp"
);
} else {
panic!("Value should still be a Date");
}
}

#[test]
fn test_truncate_date_already_truncated() {
let date_str = "2026-01-17T03:09:58Z";
let date = plist::Date::from_xml_format(date_str).unwrap();
let original_format = date.to_xml_format();
let mut value = plist::Value::Date(date);

truncate_dates_to_seconds(&mut value);

if let plist::Value::Date(truncated_date) = value {
let result = truncated_date.to_xml_format();
assert_eq!(
result, original_format,
"Already truncated date should remain unchanged"
);
}
}

#[test]
fn test_truncate_dates_in_array() {
let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap();
let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap();
let mut value =
plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]);

truncate_dates_to_seconds(&mut value);

if let plist::Value::Array(arr) = value {
for item in arr {
if let plist::Value::Date(date) = item {
let formatted = date.to_xml_format();
assert!(
!formatted.contains('.'),
"Dates in array should be truncated"
);
}
}
}
}

#[test]
fn test_truncate_dates_in_dictionary() {
let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap();
let mut dict = plist::Dictionary::new();
dict.insert("timestamp".to_string(), plist::Value::Date(date));
let mut value = plist::Value::Dictionary(dict);

truncate_dates_to_seconds(&mut value);

if let plist::Value::Dictionary(dict) = value {
if let Some(plist::Value::Date(date)) = dict.get("timestamp") {
let formatted = date.to_xml_format();
assert!(
!formatted.contains('.'),
"Date in dictionary should be truncated"
);
}
}
}

#[test]
fn test_other_value_types_unchanged() {
let mut string_val = plist::Value::String("test".to_string());
let mut int_val = plist::Value::Integer(42.into());
let mut bool_val = plist::Value::Boolean(true);

truncate_dates_to_seconds(&mut string_val);
truncate_dates_to_seconds(&mut int_val);
truncate_dates_to_seconds(&mut bool_val);

assert!(matches!(string_val, plist::Value::String(_)));
assert!(matches!(int_val, plist::Value::Integer(_)));
assert!(matches!(bool_val, plist::Value::Boolean(_)));
}
}
Loading