diff --git a/appmanifest/appmanifest.go b/appmanifest/appmanifest.go new file mode 100644 index 00000000..d5df6f5f --- /dev/null +++ b/appmanifest/appmanifest.go @@ -0,0 +1,126 @@ +// package appmanifest provides utilities for managing app manifest files +// used by MDM InstallApplication commands. +package appmanifest + +import ( + "crypto/md5" + "fmt" + "io" + + "github.com/pkg/errors" +) + +// DefaultMD5Size is the default size of each file chunk that needs to be hashed +const DefaultMD5Size = 10 << 20 // 10MB + +// http://help.apple.com/deployment/osx/#/ior5df10f73a +type Manifest struct { + ManifestItems []Item `plist:"items"` +} + +type Item struct { + Assets []Asset `plist:"assets"` + // Apple claims the metadata struct is required, + // but testing shows otherwise. + Metadata *Metadata `plist:"metadata,omitempty"` +} + +type Asset struct { + Kind string `plist:"kind"` + MD5Size int64 `plist:"md5-size"` + MD5s []string `plist:"md5s"` + URL string `plist:"url"` +} + +type Metadata struct { + BundleInfo + Items []BundleInfo `plist:"items,omitempty"` + Kind string `plist:"kind"` + Subtitle string `plist:"subtitle"` + Title string `plist:"title"` +} + +type BundleInfo struct { + BundleIdentifier string `plist:"bundle-identifier"` + BundleVersion string `plist:"bundle-version"` +} + +// File is an io.Reader which knows its size. +type File interface { + io.Reader + Size() int64 +} + +type Option func(*config) + +// WithMD5Size overrides the DefaultMD5Size when creating an AppManifest. +func WithMD5Size(md5Size int64) Option { + return func(c *config) { + c.md5Size = md5Size + } +} + +type config struct { + md5Size int64 +} + +// Create an AppManifest and write it to an io.Writer. +func Create(file File, url string, opts ...Option) (*Manifest, error) { + c := config{ + md5Size: DefaultMD5Size, + } + + for _, opt := range opts { + opt(&c) + } + + fSize := file.Size() + if c.md5Size > fSize { + c.md5Size = fSize + } + + // create a list of md5s + md5s, err := calculateMD5s(file, c.md5Size) + if err != nil { + return nil, errors.Wrap(err, "calculate appmanifest md5s") + } + + // create an asset + ast := Asset{ + Kind: "software-package", + MD5Size: c.md5Size, + MD5s: md5s, + URL: url, + } + + // make a manifest + m := Manifest{ + ManifestItems: []Item{ + Item{ + Assets: []Asset{ast}, + }, + }, + } + + return &m, nil +} + +// reads a file and returns a slice of hashes, one for each +// 10mb chunk +func calculateMD5s(f io.Reader, s int64) ([]string, error) { + h := md5.New() + var md5s []string + for { + n, err := io.CopyN(h, f, s) + if n > 0 { + md5s = append(md5s, fmt.Sprintf("%x", h.Sum(nil))) + h.Reset() + } + if err != nil { + if err == io.EOF { + err = nil + } + return md5s, err + } + } +} diff --git a/appmanifest/appmanifest_test.go b/appmanifest/appmanifest_test.go new file mode 100644 index 00000000..754e8374 --- /dev/null +++ b/appmanifest/appmanifest_test.go @@ -0,0 +1,56 @@ +package appmanifest + +import ( + "bytes" + "testing" +) + +func TestCalucalteMD5s(t *testing.T) { + ok := []string{ + "f1c9645dbc14efddc7d8a322685f26eb", + "f1c9645dbc14efddc7d8a322685f26eb", + "f1c9645dbc14efddc7d8a322685f26eb", + "93b885adfe0da089cdf634904fd59f71", + } + buf := make([]byte, DefaultMD5Size*3+1) + r := bytes.NewReader(buf) + md5s, err := calculateMD5s(r, DefaultMD5Size) + if err != nil { + t.Fatal(err) + } + if len(md5s) != len(ok) { + t.Fatal("expected", len(ok), "got", len(md5s)) + } + for i, h := range md5s { + if ok[i] != h { + t.Fatal("expected", ok[i], "got", h) + } + } +} + +/* +Benchmark10MB-8 500000 3297 ns/op 32896 B/op 3 allocs/op +Benchmark100MB-8 300000 3528 ns/op 32897 B/op 3 allocs/op +Benchmark1000MB-8 1 1967685209 ns/op 3334064 B/op 829 allocs/op +*/ + +func benchmarkSize(b *testing.B, size int) { + var buf = make([]byte, size) + r := bytes.NewReader(buf) + b.ResetTimer() + for i := 0; i < b.N; i++ { + calculateMD5s(r, DefaultMD5Size) + } +} + +func Benchmark10MB(b *testing.B) { + benchmarkSize(b, DefaultMD5Size) +} + +func Benchmark100MB(b *testing.B) { + benchmarkSize(b, DefaultMD5Size*10) +} + +func Benchmark1000MB(b *testing.B) { + benchmarkSize(b, DefaultMD5Size*100) +}