create appmanifest package (#192)

create appmanifest package for managing AppManifests
This commit is contained in:
Victor Vrantchan
2017-05-30 22:11:46 -04:00
committed by GitHub
parent a0d1498b1b
commit 7150bf083b
2 changed files with 182 additions and 0 deletions

126
appmanifest/appmanifest.go Normal file
View File

@@ -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
}
}
}

View File

@@ -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)
}