@@ -300,6 +300,102 @@ func TestNormalizeMaterial(t *testing.T) {
300300 }
301301}
302302
303+ // TestBinaryContentStructRoundTrip verifies that binary (non-UTF-8) content in
304+ // ResourceDescriptor.Content survives the structpb.Struct round-trip that happens
305+ // during predicate() → extractPredicate(): json.Marshal → protojson.Unmarshal(Struct) →
306+ // protojson.Marshal(Struct) → json.Unmarshal.
307+ func TestBinaryContentStructRoundTrip (t * testing.T ) {
308+ testCases := []struct {
309+ name string
310+ content []byte
311+ }{
312+ {
313+ name : "null bytes and high bytes" ,
314+ content : []byte {0xff , 0xfe , 0x00 , 0x01 , 0x80 , 0x7f },
315+ },
316+ {
317+ name : "ELF header" ,
318+ content : []byte {0x7f , 0x45 , 0x4c , 0x46 , 0x02 , 0x01 , 0x01 , 0x00 },
319+ },
320+ {
321+ name : "all byte values" ,
322+ content : func () []byte {
323+ b := make ([]byte , 256 )
324+ for i := range b {
325+ b [i ] = byte (i )
326+ }
327+ return b
328+ }(),
329+ },
330+ {
331+ name : "valid UTF-8 text" ,
332+ content : []byte ("hello world" ),
333+ },
334+ {
335+ name : "large binary payload" ,
336+ content : func () []byte {
337+ b := make ([]byte , 64 * 1024 )
338+ for i := range b {
339+ b [i ] = byte (i % 251 )
340+ }
341+ return b
342+ }(),
343+ },
344+ }
345+
346+ for _ , tc := range testCases {
347+ t .Run (tc .name , func (t * testing.T ) {
348+ // Build a minimal predicate with one inline artifact material
349+ original := & ProvenancePredicateV02 {
350+ ProvenancePredicateCommon : & ProvenancePredicateCommon {},
351+ Materials : []* intoto.ResourceDescriptor {
352+ {
353+ Name : "binary.bin" ,
354+ Annotations : mapToStruct (t , map [string ]interface {}{
355+ "chainloop.material.name" : "test-binary" ,
356+ "chainloop.material.type" : "ARTIFACT" ,
357+ "chainloop.material.cas.inline" : true ,
358+ }),
359+ Digest : map [string ]string {"sha256" : "deadbeef" },
360+ Content : tc .content ,
361+ },
362+ },
363+ }
364+
365+ // === Step 1: predicate() path ===
366+ // json.Marshal base64-encodes []byte fields
367+ predicateJSON , err := json .Marshal (original )
368+ require .NoError (t , err )
369+
370+ // protojson.Unmarshal stores the base64 string in Struct
371+ predStruct := & structpb.Struct {}
372+ err = protojson .Unmarshal (predicateJSON , predStruct )
373+ require .NoError (t , err )
374+
375+ // === Step 2: extractPredicate() path ===
376+ // protojson.Marshal outputs the Struct as JSON
377+ roundTrippedJSON , err := protojson .Marshal (predStruct )
378+ require .NoError (t , err )
379+
380+ // json.Unmarshal decodes base64 back into []byte
381+ var restored ProvenancePredicateV02
382+ err = json .Unmarshal (roundTrippedJSON , & restored )
383+ require .NoError (t , err )
384+
385+ require .Len (t , restored .Materials , 1 )
386+ assert .Equal (t , tc .content , restored .Materials [0 ].Content ,
387+ "binary content should survive structpb.Struct round-trip" )
388+
389+ // === Step 3: normalizeMaterial() ===
390+ normalized , err := normalizeMaterial (restored .Materials [0 ])
391+ require .NoError (t , err )
392+ assert .Equal (t , tc .content , normalized .Value ,
393+ "binary content should survive normalization" )
394+ assert .True (t , normalized .EmbeddedInline )
395+ })
396+ }
397+ }
398+
303399func TestStructValueToString (t * testing.T ) {
304400 testCases := []struct {
305401 name string
0 commit comments