Browse Source

Merge 0d7f924782 into 0cefa4cca7

pull/7583/merge
xhon-pelushi 2 days ago
committed by GitHub
parent
commit
2e65f8b0c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      src/api/core/ciphers.rs
  2. 94
      src/util.rs

18
src/api/core/ciphers.rs

@ -25,7 +25,7 @@ use crate::{
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
}, },
}, },
util::{NumberOrString, deser_opt_nonempty_str, save_temp_file}, util::{NumberOrString, deser_double_opt_str, deser_opt_nonempty_str, save_temp_file},
}; };
use super::folders::FolderData; use super::folders::FolderData;
@ -297,7 +297,9 @@ pub struct CipherData {
// when using older client versions, or if the operation doesn't involve // when using older client versions, or if the operation doesn't involve
// updating an existing cipher. // updating an existing cipher.
last_known_revision_date: Option<String>, last_known_revision_date: Option<String>,
archived_date: Option<String>, // Absent = leave archive state unchanged; null = unarchive; string = set archived date.
#[serde(default, deserialize_with = "deser_double_opt_str")]
archived_date: Option<Option<String>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -537,11 +539,17 @@ pub async fn update_cipher_from_data(
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?; cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?; cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;
if let Some(dt_str) = data.archived_date { match &data.archived_date {
match NaiveDateTime::parse_from_str(&dt_str, "%+") { // Field omitted: leave archive state unchanged.
None => {}
// Explicit JSON null: unarchive (matches Bitwarden cloud).
Some(None) => {
cipher.unarchive(&headers.user.uuid, conn).await?;
}
Some(Some(dt_str)) => match NaiveDateTime::parse_from_str(dt_str, "%+") {
Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?, Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?,
Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"), Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"),
} },
} }
if ut != UpdateType::None { if ut != UpdateType::None {

94
src/util.rs

@ -680,6 +680,100 @@ where
})) }))
} }
/// Deserialize a field that can be absent, JSON `null`, or a string.
///
/// - Field absent → `None` (caller should leave the existing value unchanged)
/// - JSON `null` → `Some(None)` (caller should clear the value)
/// - JSON string → `Some(Some(value))`
///
/// Needed because plain `Option<String>` collapses both absent and `null` into `None`.
pub fn deser_double_opt_str<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
where
D: Deserializer<'de>,
{
use serde::Deserialize;
use serde::de::{self, Visitor};
use std::fmt;
struct DoubleOptStrVisitor;
impl<'de> Visitor<'de> for DoubleOptStrVisitor {
type Value = Option<Option<String>>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string or null")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(Some(value.to_owned())))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(Some(value)))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(None))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Some(None))
}
fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
where
D2: Deserializer<'de>,
{
Ok(Some(Some(String::deserialize(deserializer)?)))
}
}
deserializer.deserialize_option(DoubleOptStrVisitor)
}
#[cfg(test)]
mod double_opt_str_tests {
use super::deser_double_opt_str;
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct Sample {
#[serde(default, deserialize_with = "deser_double_opt_str")]
archived_date: Option<Option<String>>,
}
#[test]
fn absent_field_means_no_change() {
let parsed: Sample = serde_json::from_str(r#"{"other":1}"#).unwrap();
assert_eq!(parsed.archived_date, None);
}
#[test]
fn null_field_means_clear() {
let parsed: Sample = serde_json::from_str(r#"{"archivedDate":null}"#).unwrap();
assert_eq!(parsed.archived_date, Some(None));
}
#[test]
fn string_field_means_set() {
let parsed: Sample = serde_json::from_str(r#"{"archivedDate":"2026-08-11T12:00:00.000Z"}"#).unwrap();
assert_eq!(parsed.archived_date, Some(Some("2026-08-11T12:00:00.000Z".to_string())));
}
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum NumberOrString { pub enum NumberOrString {

Loading…
Cancel
Save