-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for normalizing the archived files metadata #47
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,18 @@ package archive | |
import ( | ||
"archive/zip" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"time" | ||
) | ||
|
||
const ( | ||
uint32max = (1 << 32) - 1 | ||
) | ||
|
||
type ZipArchiver struct { | ||
filepath string | ||
filewriter *os.File | ||
|
@@ -22,22 +27,35 @@ func NewZipArchiver(filepath string) Archiver { | |
} | ||
} | ||
|
||
func (a *ZipArchiver) ArchiveContent(content []byte, infilename string) error { | ||
func (a *ZipArchiver) ArchiveContent(content []byte, infilename string, normalizeFilesMetadata bool) error { | ||
if err := a.open(); err != nil { | ||
return err | ||
} | ||
defer a.close() | ||
|
||
f, err := a.writer.Create(filepath.ToSlash(infilename)) | ||
if err != nil { | ||
return err | ||
var f io.Writer | ||
var err error | ||
|
||
if normalizeFilesMetadata { | ||
fh := prepareEmptyHeader(content, infilename) | ||
normalizeCompressingFile(fh) | ||
|
||
f, err = a.writer.CreateHeader(fh) | ||
if err != nil { | ||
return fmt.Errorf("error creating file inside archive: %s", err) | ||
} | ||
} else { | ||
f, err = a.writer.Create(filepath.ToSlash(infilename)) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
_, err = f.Write(content) | ||
return err | ||
} | ||
|
||
func (a *ZipArchiver) ArchiveFile(infilename string) error { | ||
func (a *ZipArchiver) ArchiveFile(infilename string, normalizeFilesMetadata bool) error { | ||
fi, err := assertValidFile(infilename) | ||
if err != nil { | ||
return err | ||
|
@@ -58,9 +76,14 @@ func (a *ZipArchiver) ArchiveFile(infilename string) error { | |
return fmt.Errorf("error creating file header: %s", err) | ||
} | ||
fh.Name = filepath.ToSlash(fi.Name()) | ||
fh.Method = zip.Deflate | ||
// fh.Modified alone isn't enough when using a zero value | ||
fh.SetModTime(time.Time{}) | ||
|
||
if normalizeFilesMetadata { | ||
normalizeCompressingFile(fh) | ||
} else { | ||
fh.Method = zip.Deflate | ||
// fh.Modified alone isn't enough when using a zero value | ||
fh.SetModTime(time.Time{}) | ||
} | ||
|
||
f, err := a.writer.CreateHeader(fh) | ||
if err != nil { | ||
|
@@ -84,7 +107,38 @@ func checkMatch(fileName string, excludes []string) (value bool) { | |
return false | ||
} | ||
|
||
func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string) error { | ||
// The basic file header is very simple. The UncompressedSize logic is not a real-world use case | ||
// in this context, but "640K ought to be enough for anybody". | ||
// | ||
// For reference, see golang/src/archive/zip/struct.go. | ||
func prepareEmptyHeader(content []byte, infilename string) *zip.FileHeader { | ||
fh := &zip.FileHeader{ | ||
Name: filepath.ToSlash(infilename), | ||
UncompressedSize64: uint64(len(content)), | ||
} | ||
|
||
if fh.UncompressedSize64 > uint32max { | ||
fh.UncompressedSize = uint32max | ||
} else { | ||
fh.UncompressedSize = uint32(fh.UncompressedSize64) | ||
} | ||
|
||
return fh | ||
} | ||
|
||
// Normalize the fields: | ||
// | ||
// - no compression, so the compressed stream is essentially a copy; | ||
// - fixed date; | ||
// - fixed file permissions. | ||
// | ||
func normalizeCompressingFile(fh *zip.FileHeader) { | ||
fh.Method = zip.Store | ||
fh.SetModTime(time.Date(1981, 4, 10, 0, 0, 0, 0, time.UTC)) | ||
fh.SetMode(0644) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the file is executable? See here for an example: https://github.com/terraform-providers/terraform-provider-archive/pull/41/files#diff-572809c26c1b9c23a665ebaea5dd7ea8R70 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with executing binaries inside a Lambda functions; based on a brief research, maybe Lambda resets the permissions of the archived files - see, for example:
It's not 100% clear from those resources where their problem is - it could be the zip library not storing the file permissions (not all do), or Lambda resetting the permissions when unarchiving; if it's the latter case, managing executable bit would not yield any effect. Do you have direct experience on this use case, so that I know with certainty what's the exact behavior? |
||
} | ||
|
||
func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string, normalizeFilesMetadata bool) error { | ||
_, err := assertValidDir(indirname) | ||
if err != nil { | ||
return err | ||
|
@@ -128,9 +182,14 @@ func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string) error { | |
return fmt.Errorf("error creating file header: %s", err) | ||
} | ||
fh.Name = filepath.ToSlash(relname) | ||
fh.Method = zip.Deflate | ||
// fh.Modified alone isn't enough when using a zero value | ||
fh.SetModTime(time.Time{}) | ||
|
||
if normalizeFilesMetadata { | ||
normalizeCompressingFile(fh) | ||
} else { | ||
fh.Method = zip.Deflate | ||
// fh.Modified alone isn't enough when using a zero value | ||
fh.SetModTime(time.Time{}) | ||
} | ||
|
||
f, err := a.writer.CreateHeader(fh) | ||
if err != nil { | ||
|
@@ -145,7 +204,7 @@ func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string) error { | |
}) | ||
} | ||
|
||
func (a *ZipArchiver) ArchiveMultiple(content map[string][]byte) error { | ||
func (a *ZipArchiver) ArchiveMultiple(content map[string][]byte, normalizeFilesMetadata bool) error { | ||
if err := a.open(); err != nil { | ||
return err | ||
} | ||
|
@@ -161,10 +220,24 @@ func (a *ZipArchiver) ArchiveMultiple(content map[string][]byte) error { | |
sort.Strings(keys) | ||
|
||
for _, filename := range keys { | ||
f, err := a.writer.Create(filepath.ToSlash(filename)) | ||
if err != nil { | ||
return err | ||
var f io.Writer | ||
var err error | ||
|
||
if normalizeFilesMetadata { | ||
fh := prepareEmptyHeader(content[filename], filename) | ||
normalizeCompressingFile(fh) | ||
|
||
f, err = a.writer.CreateHeader(fh) | ||
if err != nil { | ||
return fmt.Errorf("error creating file inside archive: %s", err) | ||
} | ||
} else { | ||
f, err = a.writer.Create(filepath.ToSlash(filename)) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
_, err = f.Write(content[filename]) | ||
if err != nil { | ||
return err | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes a very sneaky compatibility issue with Java's standard java.util.zip, resulting in:
The quick summary seems to be: Go's zip uses streaming, meaning it first writes the file entry body, and then writes a so-called "ext" data descriptor with size information. Then, java.util.zip can extract these if they use DEFLATE, since DEFLATE encodes data in such a way that you can know when you have all the data. STORE, on the other hand, has no such encoded info, and Java fails to handle it.
Details:
Impact case: The Amazon ECS (Blue/Green) deploy provider of AWS CodePipeline raises an exception if a normalized zip file is fed to it:
We use archive_file to produce the zip, containing appspec.yaml and taskdef.json files for CodeDeploy and ECS, respectively.
AWS services seem to be heavily Java based, so while we don't see the full Java exception output, it seems likely that this is the problem.