From 23ae99a339ffb340e039a087aad05634a9d49214 Mon Sep 17 00:00:00 2001 From: Simon Leung Date: Wed, 18 Nov 2015 09:51:50 -0800 Subject: [PATCH] Add support for prefixed bytes to zipper See https://github.com/cloudfoundry/cli/issues/411#issuecomment-93034964 for background. [#93671532] Signed-off-by: Kris Hicks --- cf/app_files/zipper.go | 172 +++++++++++++++++++++++++++++++----- cf/app_files/zipper_test.go | 161 ++++++++++++++++++++++++++++++--- 2 files changed, 297 insertions(+), 36 deletions(-) diff --git a/cf/app_files/zipper.go b/cf/app_files/zipper.go index 2eefb4d7da..827e2e5d46 100644 --- a/cf/app_files/zipper.go +++ b/cf/app_files/zipper.go @@ -2,6 +2,8 @@ package app_files import ( "archive/zip" + "bufio" + "bytes" "io" "os" "path/filepath" @@ -29,9 +31,57 @@ func (zipper ApplicationZipper) Zip(dirOrZipFile string, targetFile *os.File) (e return } -func (zipper ApplicationZipper) IsZipFile(file string) (result bool) { - _, err := zip.OpenReader(file) - return err == nil +func (zipper ApplicationZipper) IsZipFile(name string) bool { + // is it a directory? + f, err := os.Open(name) + if err != nil { + return false + } + + fi, err := f.Stat() + if err != nil { + return false + } + + if fi.IsDir() { + return false + } + + // is it a zip file? + _, err = zip.OpenReader(name) + if err == nil { + return true + } + + // is it a zip file with an offset file header signature? + if err == zip.ErrFormat { + loc, err := zipper.zipFileHeaderLocation(name) + if err != nil { + return false + } + + if loc > int64(-1) { + f, err := os.Open(name) + if err != nil { + return false + } + + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return false + } + + readerAt := io.NewSectionReader(f, loc, fi.Size()) + _, err = zip.NewReader(readerAt, fi.Size()) + if err == nil { + return true + } + } + } + + return false } func writeZipFile(dir string, targetFile *os.File) error { @@ -78,46 +128,120 @@ func writeZipFile(dir string, targetFile *os.File) error { }) } -func (zipper ApplicationZipper) Unzip(appDir string, destDir string) error { - r, err := zip.OpenReader(appDir) +func (zipper ApplicationZipper) extractFile(f *zip.File, destDir string) error { + if f.FileInfo().IsDir() { + os.MkdirAll(filepath.Join(destDir, f.Name), os.ModeDir|os.ModePerm) + return nil + } + + var rc io.ReadCloser + rc, err := f.Open() if err != nil { return err } - defer r.Close() - for _, f := range r.File { - // anonymous func allows the defer of rc.Close() - err = func() error { - if f.FileInfo().IsDir() { - os.MkdirAll(filepath.Join(destDir, f.Name), os.ModeDir|os.ModePerm) - return nil - } + defer rc.Close() + + destFilePath := filepath.Join(destDir, f.Name) + + err = fileutils.CopyReaderToPath(rc, destFilePath) + if err != nil { + return err + } + + err = os.Chmod(destFilePath, f.FileInfo().Mode()) + if err != nil { + return err + } - var rc io.ReadCloser - rc, err = f.Open() + return nil +} + +func (zipper ApplicationZipper) zipFileHeaderLocation(name string) (int64, error) { + f, err := os.Open(name) + if err != nil { + return -1, err + } + + defer f.Close() + + // zip file header signature, 0x04034b50, reversed due to little-endian byte order + firstByte := byte(0x50) + restBytes := []byte{0x4b, 0x03, 0x04} + count := int64(-1) + foundAt := int64(-1) + + reader := bufio.NewReader(f) + + keepGoing := true + for keepGoing { + count++ + + b, err := reader.ReadByte() + if err != nil { + keepGoing = false + break + } + + if b == firstByte { + nextBytes, err := reader.Peek(3) if err != nil { - return err + keepGoing = false } + if bytes.Compare(nextBytes, restBytes) == 0 { + foundAt = count + keepGoing = false + break + } + } + } - defer rc.Close() + return foundAt, nil +} - destFilePath := filepath.Join(destDir, f.Name) +func (zipper ApplicationZipper) Unzip(name string, destDir string) error { + rc, err := zip.OpenReader(name) - err = fileutils.CopyReaderToPath(rc, destFilePath) + if err == nil { + defer rc.Close() + for _, f := range rc.File { + err := zipper.extractFile(f, destDir) if err != nil { return err } + } + } - err = os.Chmod(destFilePath, f.FileInfo().Mode()) + if err == zip.ErrFormat { + loc, err := zipper.zipFileHeaderLocation(name) + if err != nil { + return err + } + + if loc > int64(-1) { + f, err := os.Open(name) if err != nil { return err } - return nil - }() + defer f.Close() - if err != nil { - return err + fi, err := f.Stat() + if err != nil { + return err + } + + readerAt := io.NewSectionReader(f, loc, fi.Size()) + r, err := zip.NewReader(readerAt, fi.Size()) + if err != nil { + return err + } + for _, f := range r.File { + err := zipper.extractFile(f, destDir) + if err != nil { + return err + } + } } } diff --git a/cf/app_files/zipper_test.go b/cf/app_files/zipper_test.go index 508a8ede4c..2876bb30c9 100644 --- a/cf/app_files/zipper_test.go +++ b/cf/app_files/zipper_test.go @@ -25,13 +25,20 @@ func readFile(file *os.File) []byte { // Thanks to Svett Ralchev // http://blog.ralch.com/tutorial/golang-working-with-zip/ -func zipit(source, target string) error { +func zipit(source, target, prefix string) error { zipfile, err := os.Create(target) if err != nil { return err } defer zipfile.Close() + if prefix != "" { + _, err = io.WriteString(zipfile, prefix) + if err != nil { + return err + } + } + archive := zip.NewWriter(zipfile) defer archive.Close() @@ -67,6 +74,7 @@ func zipit(source, target string) error { return err } defer file.Close() + _, err = io.Copy(writer, file) return err }) @@ -172,13 +180,147 @@ var _ = Describe("Zipper", func() { }) }) + Describe("IsZipFile", func() { + var ( + inDir, outDir string + zipper ApplicationZipper + ) + + AfterEach(func() { + os.RemoveAll(inDir) + os.RemoveAll(outDir) + }) + + Context("when given a zip without prefix bytes", func() { + BeforeEach(func() { + var err error + inDir, err = ioutil.TempDir("", "zipper-unzip-in") + Expect(err).NotTo(HaveOccurred()) + + err = ioutil.WriteFile(path.Join(inDir, "file1"), []byte("file-1-contents"), 0664) + Expect(err).NotTo(HaveOccurred()) + + outDir, err = ioutil.TempDir("", "zipper-unzip-out") + Expect(err).NotTo(HaveOccurred()) + + err = zipit(path.Join(inDir, "/"), path.Join(outDir, "out.zip"), "") + Expect(err).NotTo(HaveOccurred()) + + zipper = ApplicationZipper{} + }) + + It("returns true", func() { + Expect(zipper.IsZipFile(path.Join(outDir, "out.zip"))).To(BeTrue()) + }) + }) + + Context("when given a zip with prefix bytes", func() { + BeforeEach(func() { + var err error + inDir, err = ioutil.TempDir("", "zipper-unzip-in") + Expect(err).NotTo(HaveOccurred()) + + err = ioutil.WriteFile(path.Join(inDir, "file1"), []byte("file-1-contents"), 0664) + Expect(err).NotTo(HaveOccurred()) + + outDir, err = ioutil.TempDir("", "zipper-unzip-out") + Expect(err).NotTo(HaveOccurred()) + + err = zipit(path.Join(inDir, "/"), path.Join(outDir, "out.zip"), "prefix-bytes") + Expect(err).NotTo(HaveOccurred()) + + zipper = ApplicationZipper{} + }) + + It("returns true", func() { + Expect(zipper.IsZipFile(path.Join(outDir, "out.zip"))).To(BeTrue()) + }) + }) + + Context("when given a file that is not a zip", func() { + var fileName string + + BeforeEach(func() { + f, err := ioutil.TempFile("", "zipper-test") + Expect(err).NotTo(HaveOccurred()) + + fi, err := f.Stat() + Expect(err).NotTo(HaveOccurred()) + fileName = fi.Name() + }) + + AfterEach(func() { + defer os.RemoveAll(fileName) + }) + + It("returns false", func() { + Expect(zipper.IsZipFile(fileName)).To(BeFalse()) + }) + }) + + Context("when given a directory", func() { + var dirName string + + BeforeEach(func() { + var err error + dirName, err = ioutil.TempDir("", "zipper-test") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + defer os.RemoveAll(dirName) + }) + + It("returns false", func() { + Expect(zipper.IsZipFile(dirName)).To(BeFalse()) + }) + }) + }) + Describe(".Unzip", func() { - Context("when the zipfile has an empty directory", func() { - var ( - inDir, outDir string - zipper ApplicationZipper - ) + var ( + inDir, outDir string + zipper ApplicationZipper + ) + + AfterEach(func() { + os.RemoveAll(inDir) + os.RemoveAll(outDir) + }) + Context("when the zipfile has prefix bytes", func() { + BeforeEach(func() { + var err error + inDir, err = ioutil.TempDir("", "zipper-unzip-in") + Expect(err).NotTo(HaveOccurred()) + + err = ioutil.WriteFile(path.Join(inDir, "file1"), []byte("file-1-contents"), 0664) + Expect(err).NotTo(HaveOccurred()) + + outDir, err = ioutil.TempDir("", "zipper-unzip-out") + Expect(err).NotTo(HaveOccurred()) + + err = zipit(path.Join(inDir, "/"), path.Join(outDir, "out.zip"), "prefix-bytes") + Expect(err).NotTo(HaveOccurred()) + + zipper = ApplicationZipper{} + }) + + It("successfully extracts the zip", func() { + destDir, err := ioutil.TempDir("", "dest-dir") + Expect(err).NotTo(HaveOccurred()) + + defer os.RemoveAll(destDir) + + err = zipper.Unzip(path.Join(outDir, "out.zip"), destDir) + Expect(err).NotTo(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "file1")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the zipfile has an empty directory", func() { BeforeEach(func() { var err error inDir, err = ioutil.TempDir("", "zipper-unzip-in") @@ -199,17 +341,12 @@ var _ = Describe("Zipper", func() { outDir, err = ioutil.TempDir("", "zipper-unzip-out") Expect(err).NotTo(HaveOccurred()) - err = zipit(path.Join(inDir, "/"), path.Join(outDir, "out.zip")) + err = zipit(path.Join(inDir, "/"), path.Join(outDir, "out.zip"), "") Expect(err).NotTo(HaveOccurred()) zipper = ApplicationZipper{} }) - AfterEach(func() { - os.RemoveAll(inDir) - os.RemoveAll(outDir) - }) - It("includes all entries from the zip file in the destination", func() { destDir, err := ioutil.TempDir("", "dest-dir") Expect(err).NotTo(HaveOccurred())