307 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
| package buildah
 | |
| 
 | |
| import (
 | |
| 	"archive/tar"
 | |
| 	"bytes"
 | |
| 	"io"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	digest "github.com/opencontainers/go-digest"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| func (c *CompositeDigester) isOpen() bool {
 | |
| 	for _, digester := range c.digesters {
 | |
| 		if tarDigester, ok := digester.(*tarDigester); ok {
 | |
| 			if tarDigester.isOpen {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func TestCompositeDigester(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name       string
 | |
| 		itemTypes  []string
 | |
| 		resultType string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:       "download",
 | |
| 			itemTypes:  []string{""},
 | |
| 			resultType: "",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "file",
 | |
| 			itemTypes:  []string{"file"},
 | |
| 			resultType: "file",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "dir",
 | |
| 			itemTypes:  []string{"dir"},
 | |
| 			resultType: "dir",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-1",
 | |
| 			itemTypes:  []string{"file", "dir"},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-2",
 | |
| 			itemTypes:  []string{"dir", "file"},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-3",
 | |
| 			itemTypes:  []string{"", "dir"},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-4",
 | |
| 			itemTypes:  []string{"", "file"},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-5",
 | |
| 			itemTypes:  []string{"dir", ""},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 		{
 | |
| 			name:       "multiple-6",
 | |
| 			itemTypes:  []string{"file", ""},
 | |
| 			resultType: "multi",
 | |
| 		},
 | |
| 	}
 | |
| 	var digester CompositeDigester
 | |
| 	var i int
 | |
| 	var buf bytes.Buffer
 | |
| 	zero := time.Unix(0, 0)
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			for _, filtered := range []bool{false, true} {
 | |
| 				desc := "unfiltered"
 | |
| 				if filtered {
 | |
| 					desc = "filter"
 | |
| 				}
 | |
| 				t.Run(desc, func(t *testing.T) {
 | |
| 					if i > 0 {
 | |
| 						// restart only after it's been used some, to make sure it's not necessary otherwise
 | |
| 						digester.Restart()
 | |
| 					}
 | |
| 					i++
 | |
| 					size := int64(i * 32) // items for this archive will be bigger than the last one
 | |
| 					for _, itemType := range test.itemTypes {
 | |
| 						for int64(buf.Len()) < size {
 | |
| 							err := buf.WriteByte(byte(buf.Len() % 256))
 | |
| 							require.Nil(t, err, "error padding content buffer: %v", err)
 | |
| 						}
 | |
| 						// feed it content that it will treat either as raw data ("") or expect to
 | |
| 						// look like a tarball ("file"/"dir")
 | |
| 						digester.Start(itemType)
 | |
| 						hasher := digester.Hash() // returns an io.WriteCloser
 | |
| 						require.NotNil(t, hasher, "digester returned a null hasher?")
 | |
| 						if itemType == "" {
 | |
| 							// write something that isn't an archive
 | |
| 							n, err := io.Copy(hasher, &buf)
 | |
| 							require.Nil(t, err, "error writing tar content to digester: %v", err)
 | |
| 							require.Equal(t, size, n, "short write writing tar content to digester")
 | |
| 							continue
 | |
| 						}
 | |
| 						// write an archive
 | |
| 						var written bytes.Buffer // a copy of the archive we're generating and digesting
 | |
| 						hasher = &struct {
 | |
| 							io.Writer
 | |
| 							io.Closer
 | |
| 						}{
 | |
| 							Writer: io.MultiWriter(hasher, &written), // splice into the writer
 | |
| 							Closer: hasher,
 | |
| 						}
 | |
| 						if filtered {
 | |
| 							// wrap the WriteCloser in another WriteCloser
 | |
| 							hasher = newTarFilterer(hasher, func(hdr *tar.Header) (bool, bool, io.Reader) {
 | |
| 								hdr.ModTime = zero
 | |
| 								return false, false, nil
 | |
| 							})
 | |
| 							require.NotNil(t, hasher, "newTarFilterer returned a null WriteCloser?")
 | |
| 						}
 | |
| 						// write this item as an archive
 | |
| 						tw := tar.NewWriter(hasher)
 | |
| 						hdr := &tar.Header{
 | |
| 							Name:     "content",
 | |
| 							Size:     size,
 | |
| 							Mode:     0640,
 | |
| 							ModTime:  time.Now(),
 | |
| 							Typeflag: tar.TypeReg,
 | |
| 						}
 | |
| 						err := tw.WriteHeader(hdr)
 | |
| 						require.Nil(t, err, "error writing tar header to digester: %v", err)
 | |
| 						n, err := io.Copy(tw, &buf)
 | |
| 						require.Nil(t, err, "error writing tar content to digester: %v", err)
 | |
| 						require.Equal(t, size, n, "short write writing tar content to digester")
 | |
| 						err = tw.Flush()
 | |
| 						require.Nil(t, err, "error flushing tar content to digester: %v", err)
 | |
| 						err = tw.Close()
 | |
| 						require.Nil(t, err, "error closing tar archive being written digester: %v", err)
 | |
| 						if filtered {
 | |
| 							// the ContentDigester can close its own if we don't explicitly ask it to,
 | |
| 							// but if we wrapped it in a filter, we have to close the filter to clean
 | |
| 							// up the filter, so we can't skip it to exercise that logic; we have to
 | |
| 							// leave that for the corresponding unfiltered case to try
 | |
| 							hasher.Close()
 | |
| 						}
 | |
| 						// now read the archive back
 | |
| 						tr := tar.NewReader(&written)
 | |
| 						require.NotNil(t, tr, "unable to read byte buffer?")
 | |
| 						hdr, err = tr.Next()
 | |
| 						for err == nil {
 | |
| 							var n int64
 | |
| 							if filtered {
 | |
| 								// the filter should have set the modtime to unix 0
 | |
| 								require.Equal(t, zero, hdr.ModTime, "timestamp for entry should have been zero")
 | |
| 							} else {
 | |
| 								// the filter should have left modtime to "roughly now"
 | |
| 								require.NotEqual(t, zero, hdr.ModTime, "timestamp for entry should not have been zero")
 | |
| 							}
 | |
| 							n, err = io.Copy(io.Discard, tr)
 | |
| 							require.Nil(t, err, "error reading tar content from buffer: %v", err)
 | |
| 							require.Equal(t, hdr.Size, n, "short read reading tar content")
 | |
| 							hdr, err = tr.Next()
 | |
| 						}
 | |
| 						require.Equal(t, io.EOF, err, "finished reading archive with %v, not EOF", err)
 | |
| 					}
 | |
| 					// check the composite digest type matches expectations and the value is not just the
 | |
| 					// digest of zero-length data, which is absolutely not what we wrote
 | |
| 					digestType, digestValue := digester.Digest()
 | |
| 					require.Equal(t, test.resultType, digestType, "expected to get a %q digest back for %v, got %q", test.resultType, test.itemTypes, digestType)
 | |
| 					require.NotEqual(t, digest.Canonical.FromBytes([]byte{}), digestValue, "digester wasn't fed any data")
 | |
| 					require.False(t, digester.isOpen(), "expected digester to have been closed with this usage pattern")
 | |
| 				})
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestTarFilterer(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name          string
 | |
| 		input, output map[string]string
 | |
| 		breakAfter    int
 | |
| 		filter        func(*tar.Header) (bool, bool, io.Reader)
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "none",
 | |
| 			input: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 			},
 | |
| 			output: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 			},
 | |
| 			filter: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "plain",
 | |
| 			input: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 			},
 | |
| 			output: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 			},
 | |
| 			filter: func(*tar.Header) (bool, bool, io.Reader) { return false, false, nil },
 | |
| 		},
 | |
| 		{
 | |
| 			name: "skip",
 | |
| 			input: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 			},
 | |
| 			output: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 			},
 | |
| 			filter: func(hdr *tar.Header) (bool, bool, io.Reader) { return hdr.Name == "file b", false, nil },
 | |
| 		},
 | |
| 		{
 | |
| 			name: "replace",
 | |
| 			input: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b",
 | |
| 				"file c": "content c",
 | |
| 			},
 | |
| 			output: map[string]string{
 | |
| 				"file a": "content a",
 | |
| 				"file b": "content b+c",
 | |
| 				"file c": "content c",
 | |
| 			},
 | |
| 			breakAfter: 2,
 | |
| 			filter: func(hdr *tar.Header) (bool, bool, io.Reader) {
 | |
| 				if hdr.Name == "file b" {
 | |
| 					content := "content b+c"
 | |
| 					hdr.Size = int64(len(content))
 | |
| 					return false, true, strings.NewReader(content)
 | |
| 				}
 | |
| 				return false, false, nil
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			var buffer bytes.Buffer
 | |
| 			tw := tar.NewWriter(&buffer)
 | |
| 			files := 0
 | |
| 			for filename, contents := range test.input {
 | |
| 				hdr := tar.Header{
 | |
| 					Name:     filename,
 | |
| 					Size:     int64(len(contents)),
 | |
| 					Typeflag: tar.TypeReg,
 | |
| 				}
 | |
| 				err := tw.WriteHeader(&hdr)
 | |
| 				require.Nil(t, err, "unexpected error from TarWriter.WriteHeader")
 | |
| 				n, err := io.CopyN(tw, strings.NewReader(contents), int64(len(contents)))
 | |
| 				require.Nil(t, err, "unexpected error copying to tar writer")
 | |
| 				require.Equal(t, int64(len(contents)), n, "unexpected write length")
 | |
| 				files++
 | |
| 				if test.breakAfter != 0 && files%test.breakAfter == 0 {
 | |
| 					// this test may have us writing multiple archives to the buffer
 | |
| 					// they should still read back as a single archive
 | |
| 					tw.Close()
 | |
| 					tw = tar.NewWriter(&buffer)
 | |
| 				}
 | |
| 			}
 | |
| 			tw.Close()
 | |
| 			output := make(map[string]string)
 | |
| 			pipeReader, pipeWriter := io.Pipe()
 | |
| 			var wg sync.WaitGroup
 | |
| 			wg.Add(1)
 | |
| 			go func() {
 | |
| 				tr := tar.NewReader(pipeReader)
 | |
| 				hdr, err := tr.Next()
 | |
| 				for err == nil {
 | |
| 					var buffer bytes.Buffer
 | |
| 					var n int64
 | |
| 					n, err = io.Copy(&buffer, tr)
 | |
| 					require.Nil(t, err, "unexpected error copying from tar reader")
 | |
| 					require.Equal(t, hdr.Size, n, "unexpected read length")
 | |
| 					output[hdr.Name] = buffer.String()
 | |
| 					hdr, err = tr.Next()
 | |
| 				}
 | |
| 				require.Equal(t, io.EOF, err, "unexpected error ended our tarstream read")
 | |
| 				pipeReader.Close()
 | |
| 				wg.Done()
 | |
| 			}()
 | |
| 			filterer := newTarFilterer(pipeWriter, test.filter)
 | |
| 			_, err := io.Copy(filterer, &buffer)
 | |
| 			require.Nil(t, err, "unexpected error copying archive through filter to reader")
 | |
| 			filterer.Close()
 | |
| 			wg.Wait()
 | |
| 			require.Equal(t, test.output, output, "got unexpected results")
 | |
| 		})
 | |
| 	}
 | |
| }
 |