Skip to content

Commit ced7e53

Browse files
authored
Merge pull request #842 from sakhisheikh/fix/xctattachment-null-userinfo
fix(nskeyedarchiver): tolerate XCUITest objects with absent fields
2 parents 3ebc297 + 13f3316 commit ced7e53

2 files changed

Lines changed: 177 additions & 24 deletions

File tree

ios/nskeyedarchiver/objectivec_classes.go

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,31 @@ type XCTAttachment struct {
327327
userInfo map[string]interface{}
328328
}
329329

330+
// resolveRef follows a keyed archive reference, returning nil when the key is
331+
// absent, is not a reference, or points outside the object table. Callers
332+
// comma-ok assert the result, so an omitted field yields a zero value.
333+
func resolveRef(object map[string]interface{}, key string, objects []interface{}) interface{} {
334+
uid, isRef := object[key].(plist.UID)
335+
if !isRef || int(uid) >= len(objects) {
336+
return nil
337+
}
338+
return objects[uid]
339+
}
340+
330341
func NewXCTAttachment(object map[string]interface{}, objects []interface{}) interface{} {
331-
lifetime := object["lifetime"].(uint64)
332-
uniformTypeIdentifier := objects[object["uniformTypeIdentifier"].(plist.UID)].(string)
333-
fileNameOverride := objects[object["fileNameOverride"].(plist.UID)].(string)
334-
timestamp := objects[object["timestamp"].(plist.UID)].(float64)
335-
name := objects[object["name"].(plist.UID)].(string)
336-
userInfo, _ := extractDictionary(objects[object["userInfo"].(plist.UID)].(map[string]interface{}), objects, 0)
342+
lifetime, _ := object["lifetime"].(uint64)
343+
uniformTypeIdentifier, _ := resolveRef(object, "uniformTypeIdentifier", objects).(string)
344+
fileNameOverride, _ := resolveRef(object, "fileNameOverride", objects).(string)
345+
timestamp, _ := resolveRef(object, "timestamp", objects).(float64)
346+
name, _ := resolveRef(object, "name", objects).(string)
347+
348+
// An absent userInfo arrives as the string "$null", not a dictionary.
349+
var userInfo map[string]interface{}
350+
if raw, isDict := resolveRef(object, "userInfo", objects).(map[string]interface{}); isDict {
351+
userInfo, _ = extractDictionary(raw, objects, 0)
352+
}
337353

338-
payloadRaw := objects[object["payload"].(plist.UID)]
339-
payload := extractAttachmentPayload(payloadRaw, objects)
354+
payload := extractAttachmentPayload(resolveRef(object, "payload", objects), objects)
340355

341356
return XCTAttachment{
342357
lifetime: lifetime,
@@ -354,7 +369,7 @@ func extractAttachmentPayload(payloadRaw interface{}, objects []interface{}) []u
354369
if !byteSliceOk {
355370
mapPayload, mapOk := payloadRaw.(map[string]interface{})
356371
if mapOk {
357-
payloadClassMap, classOk := objects[mapPayload["$class"].(plist.UID)].(map[string]interface{})
372+
payloadClassMap, classOk := resolveRef(mapPayload, "$class", objects).(map[string]interface{})
358373
if classOk {
359374
payloadClass := payloadClassMap["$classname"]
360375
if payloadClass == "NSMutableData" || payloadClass == "NSData" {
@@ -772,14 +787,14 @@ type XCTIssue struct {
772787
}
773788

774789
func NewXCTIssue(object map[string]interface{}, objects []interface{}) interface{} {
775-
runtimeIssueSeverity := object["runtimeIssueSeverity"].(uint64)
776-
detailedDescriptionRef := object["detailed-description"].(plist.UID)
777-
sourceCodeContextRef := object["source-code-context"].(plist.UID)
778-
compactDescriptionRef := object["compact-description"].(plist.UID)
790+
runtimeIssueSeverity, _ := object["runtimeIssueSeverity"].(uint64)
791+
detailedDescription, _ := resolveRef(object, "detailed-description", objects).(string)
792+
compactDescription, _ := resolveRef(object, "compact-description", objects).(string)
779793

780-
detailedDescription := objects[detailedDescriptionRef].(string)
781-
compactDescription := objects[compactDescriptionRef].(string)
782-
sourceCodeContext := NewXCTSourceCodeContext(objects[sourceCodeContextRef].(map[string]interface{}), objects).(XCTSourceCodeContext)
794+
var sourceCodeContext XCTSourceCodeContext
795+
if raw, isDict := resolveRef(object, "source-code-context", objects).(map[string]interface{}); isDict {
796+
sourceCodeContext, _ = NewXCTSourceCodeContext(raw, objects).(XCTSourceCodeContext)
797+
}
783798

784799
return XCTIssue{RuntimeIssueSeverity: runtimeIssueSeverity, DetailedDescription: detailedDescription, CompactDescription: compactDescription, SourceCodeContext: sourceCodeContext}
785800
}
@@ -798,8 +813,10 @@ type XCTSourceCodeContext struct {
798813
}
799814

800815
func NewXCTSourceCodeContext(object map[string]interface{}, objects []interface{}) interface{} {
801-
locationRef := object["location"].(plist.UID)
802-
location := NewXCTSourceCodeLocation(objects[locationRef].(map[string]interface{}), objects).(XCTSourceCodeLocation)
816+
var location XCTSourceCodeLocation
817+
if raw, isDict := resolveRef(object, "location", objects).(map[string]interface{}); isDict {
818+
location, _ = NewXCTSourceCodeLocation(raw, objects).(XCTSourceCodeLocation)
819+
}
803820

804821
return XCTSourceCodeContext{Location: location}
805822
}
@@ -810,13 +827,13 @@ type XCTSourceCodeLocation struct {
810827
}
811828

812829
func NewXCTSourceCodeLocation(object map[string]interface{}, objects []interface{}) interface{} {
813-
fileUrlRef := object["file-url"].(plist.UID)
814-
relativeRef := objects[fileUrlRef].(map[string]interface{})["NS.relative"].(plist.UID)
815-
relativePath := objects[int(relativeRef)].(string)
816-
fileUrl := NewNSURL(relativePath)
817-
lineNumber := object["line-number"].(uint64)
830+
var relativePath string
831+
if fileURL, isDict := resolveRef(object, "file-url", objects).(map[string]interface{}); isDict {
832+
relativePath, _ = resolveRef(fileURL, "NS.relative", objects).(string)
833+
}
834+
lineNumber, _ := object["line-number"].(uint64)
818835

819-
return XCTSourceCodeLocation{FileUrl: fileUrl, LineNumber: lineNumber}
836+
return XCTSourceCodeLocation{FileUrl: NewNSURL(relativePath), LineNumber: lineNumber}
820837
}
821838

822839
func toInterfaceSliceOfTests(testSlice []XCTTestIdentifier) []interface{} {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package nskeyedarchiver
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
"howett.net/plist"
9+
)
10+
11+
// Attachments with no userInfo encode it as the string "$null". Asserting a
12+
// dictionary there panicked, and Unarchive turned that into an error that failed
13+
// the whole XCUITest run.
14+
func TestNewXCTAttachmentWithNullUserInfo(t *testing.T) {
15+
objects := []interface{}{
16+
"$null", // 0
17+
"public.plain-text", // 1 uniformTypeIdentifier
18+
"override.txt", // 2 fileNameOverride
19+
float64(770474977.9), // 3 timestamp
20+
"Screenshot", // 4 name
21+
[]uint8{0x61, 0x62}, // 5 payload
22+
}
23+
object := map[string]interface{}{
24+
"lifetime": uint64(1),
25+
"uniformTypeIdentifier": plist.UID(1),
26+
"fileNameOverride": plist.UID(2),
27+
"timestamp": plist.UID(3),
28+
"name": plist.UID(4),
29+
"payload": plist.UID(5),
30+
"userInfo": plist.UID(0), // points at "$null"
31+
}
32+
33+
var decoded interface{}
34+
require.NotPanics(t, func() { decoded = NewXCTAttachment(object, objects) })
35+
36+
attachment, ok := decoded.(XCTAttachment)
37+
require.True(t, ok)
38+
assert.Equal(t, "public.plain-text", attachment.UniformTypeIdentifier)
39+
assert.Equal(t, "Screenshot", attachment.Name)
40+
assert.Equal(t, []uint8{0x61, 0x62}, attachment.Payload)
41+
assert.Nil(t, attachment.userInfo, "a missing userInfo should decode as empty, not panic")
42+
}
43+
44+
// A real userInfo dictionary must still be decoded.
45+
func TestNewXCTAttachmentWithUserInfoDictionary(t *testing.T) {
46+
objects := []interface{}{
47+
"$null",
48+
"public.png",
49+
"shot.png",
50+
float64(1),
51+
"Shot",
52+
[]uint8{0x01},
53+
map[string]interface{}{"NS.keys": []interface{}{}, "NS.objects": []interface{}{}},
54+
}
55+
object := map[string]interface{}{
56+
"lifetime": uint64(2),
57+
"uniformTypeIdentifier": plist.UID(1),
58+
"fileNameOverride": plist.UID(2),
59+
"timestamp": plist.UID(3),
60+
"name": plist.UID(4),
61+
"payload": plist.UID(5),
62+
"userInfo": plist.UID(6),
63+
}
64+
65+
var decoded interface{}
66+
require.NotPanics(t, func() { decoded = NewXCTAttachment(object, objects) })
67+
attachment, ok := decoded.(XCTAttachment)
68+
require.True(t, ok)
69+
assert.Equal(t, "public.png", attachment.UniformTypeIdentifier)
70+
assert.NotNil(t, attachment.userInfo)
71+
}
72+
73+
// A reference past the end of the object table would index out of range.
74+
func TestNewXCTAttachmentToleratesMalformedArchives(t *testing.T) {
75+
objects := []interface{}{"$null", "public.png"}
76+
77+
tests := []struct {
78+
name string
79+
object map[string]interface{}
80+
}{
81+
{name: "empty object", object: map[string]interface{}{}},
82+
{
83+
name: "reference out of range",
84+
object: map[string]interface{}{
85+
"lifetime": uint64(1),
86+
"name": plist.UID(99),
87+
},
88+
},
89+
}
90+
for _, tt := range tests {
91+
t.Run(tt.name, func(t *testing.T) {
92+
require.NotPanics(t, func() { NewXCTAttachment(tt.object, objects) })
93+
})
94+
}
95+
}
96+
97+
// The same "$null" shape reaches XCTIssue and its nested source code context.
98+
func TestNewXCTIssueWithNullSourceCodeContext(t *testing.T) {
99+
objects := []interface{}{"$null", "compact", "detailed"}
100+
object := map[string]interface{}{
101+
"runtimeIssueSeverity": uint64(1),
102+
"compact-description": plist.UID(1),
103+
"detailed-description": plist.UID(2),
104+
"source-code-context": plist.UID(0), // "$null"
105+
}
106+
107+
var decoded interface{}
108+
require.NotPanics(t, func() { decoded = NewXCTIssue(object, objects) })
109+
110+
issue, ok := decoded.(XCTIssue)
111+
require.True(t, ok)
112+
assert.Equal(t, "compact", issue.CompactDescription)
113+
assert.Equal(t, "detailed", issue.DetailedDescription)
114+
}
115+
116+
func TestNewXCTSourceCodeContextWithNullLocation(t *testing.T) {
117+
objects := []interface{}{"$null"}
118+
119+
require.NotPanics(t, func() {
120+
NewXCTSourceCodeContext(map[string]interface{}{"location": plist.UID(0)}, objects)
121+
})
122+
require.NotPanics(t, func() {
123+
NewXCTSourceCodeContext(map[string]interface{}{}, objects)
124+
})
125+
}
126+
127+
func TestNewXCTSourceCodeLocationToleratesMissingFileURL(t *testing.T) {
128+
objects := []interface{}{"$null", map[string]interface{}{"NS.relative": plist.UID(0)}}
129+
130+
require.NotPanics(t, func() {
131+
NewXCTSourceCodeLocation(map[string]interface{}{"file-url": plist.UID(0), "line-number": uint64(3)}, objects)
132+
})
133+
require.NotPanics(t, func() {
134+
NewXCTSourceCodeLocation(map[string]interface{}{"file-url": plist.UID(1)}, objects)
135+
})
136+
}

0 commit comments

Comments
 (0)