Skip to content

Parse legacy CC-Things-Tag-* and recurring instance IDs - #11

Open
BenEmdon wants to merge 1 commit into
evanpurkhiser:mainfrom
BenEmdon:fix/legacy-things-id-parsing
Open

BenEmdon wants to merge 1 commit into
evanpurkhiser:mainfrom
BenEmdon:fix/legacy-things-id-parsing

Conversation

@BenEmdon

@BenEmdon BenEmdon commented Oct 4, 2026

Copy link
Copy Markdown

Fixes #10.

Accounts with data from old Things versions cannot sync at all. Two legacy ID formats break ThingsId::from_str, and a single bad entry aborts the whole append-log fold:

  • CC-Things-Tag-Office (and Errand, Home, Important, Pending): built-in tag IDs, appear in tg fields.
  • <UUID>-<YYYYMMDD>: materialized recurring-task instance IDs, template UUID plus instance date. Appear as object keys and in tombstone dloid fields.

Repro: run things3 today (any command that syncs) against such an account. It fails with e.g. Corrupt log entry at .../things.log byte 42872: invalid Things ID: "CC-Things-Tag-Office".

The fix canonicalizes both formats the same way ACTIONGROUP- IDs already are: digest the full string with SHA1 truncated to 16 bytes, so entity keys and references agree deterministically. For the instance form it only applies when the base parses as a UUID and the suffix is exactly 8 digits, so genuinely malformed IDs still error. Instances stay distinct from each other and from their template (the template link travels in the rt field as a bare UUID).

Tests: 6 new unit tests covering determinism, distinctness, serde roundtrip, and rejection of bad inputs. Full suite passes (cargo test --locked).

One caveat: unlike ACTIONGROUP-, there is no bare-UUID form the server recognizes for these IDs, so writes would re-encode the digested form. Reads are unaffected.

Comment thread src/ids/things_id.rs
Comment on lines +113 to +123
if let Some((base, date)) = s.rsplit_once('-') {
if date.len() == 8
&& date.bytes().all(|b| b.is_ascii_digit())
&& Uuid::parse_str(base).is_ok()
{
let digest = Sha1::digest(s.as_bytes());
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&digest[..16]);
return Ok(ThingsId(bytes));
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

nit and maybe clippy will catch, but I think you could write this as

Suggested change
if let Some((base, date)) = s.rsplit_once('-') {
if date.len() == 8
&& date.bytes().all(|b| b.is_ascii_digit())
&& Uuid::parse_str(base).is_ok()
{
let digest = Sha1::digest(s.as_bytes());
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&digest[..16]);
return Ok(ThingsId(bytes));
}
}
if let Some((base, date)) = s.rsplit_once('-')
&& date.len() == 8
&& date.bytes().all(|b| b.is_ascii_digit())
&& Uuid::parse_str(base).is_ok()
{
let digest = Sha1::digest(s.as_bytes());
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&digest[..16]);
return Ok(ThingsId(bytes));
}

via let chains https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/#let-chains

@evanpurkhiser

Copy link
Copy Markdown
Owner

Thanks very much for this fix!

If you can address the lint errors we can get this merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sync fails with invalid Things ID on accounts with old data

2 participants