iclouddrive: fix "cannot unmarshal number" error when listing photo albums
CloudKit is inconsistent about how it encodes the isDeleted field on
album records, returning a JSON boolean (true/false) for some accounts
and a number (0/1) for others. The numeric form caused listing of a
photo library to fail with:
json: cannot unmarshal number into Go struct field
ckBoolField.records.fields.isDeleted.value of type bool
The encoding also varies over time, not just per account: a full HTTP
dump from the reporting user showed the server sending
"isDeleted" : { "value" : 0, "type" : "INT64" }
but the same account later reverted to the boolean encoding with no
client change. Asset records already deliver isDeleted as a number, so
both encodings are in active use server side and either may appear.
Accept both encodings when parsing CloudKit boolean fields.
See: https://forum.rclone.org/t/error-when-trying-to-list-contents-of-primarysync-directory-in-icloud-photos/54028
This commit is contained in:
@@ -1649,6 +1649,29 @@ type ckBoolField struct {
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a CloudKit boolean field, accepting both the
|
||||
// boolean (true/false) and numeric (0/1) encodings the server uses
|
||||
func (b *ckBoolField) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(raw.Value) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(raw.Value, &b.Value); err == nil {
|
||||
return nil
|
||||
}
|
||||
var n float64
|
||||
if err := json.Unmarshal(raw.Value, &n); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal %q as CloudKit bool", raw.Value)
|
||||
}
|
||||
b.Value = n != 0
|
||||
return nil
|
||||
}
|
||||
|
||||
type ckReferenceField struct {
|
||||
Value struct {
|
||||
RecordName string `json:"recordName"`
|
||||
|
||||
@@ -1283,3 +1283,34 @@ func TestAlbumCacheKey(t *testing.T) {
|
||||
assert.True(t, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'), "key should be hex: %c", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCkBoolFieldUnmarshal(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{`{"value": true}`, true, false},
|
||||
{`{"value": false}`, false, false},
|
||||
{`{"value": 1}`, true, false},
|
||||
{`{"value": 0}`, false, false},
|
||||
{`{"value": "INVALID"}`, false, true},
|
||||
} {
|
||||
var field ckBoolField
|
||||
err := json.Unmarshal([]byte(test.in), &field)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, test.in)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, test.in)
|
||||
assert.Equal(t, test.want, field.Value, test.in)
|
||||
}
|
||||
|
||||
// isDeleted arrives as a number (0/1) on some accounts and as a
|
||||
// boolean on others, so album records must parse both
|
||||
var record albumRecord
|
||||
err := json.Unmarshal([]byte(`{"recordName": "album1", "fields": {"isDeleted": {"value": 1}}}`), &record)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, record.Fields.IsDeleted)
|
||||
assert.True(t, record.Fields.IsDeleted.Value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user