diff --git a/.gitignore b/.gitignore index 63fecf3..f729b51 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ bin *-initrd.img *-kernel +*.img dist diff --git a/pkg/api/build.go b/pkg/api/build.go index 02f472c..dc4e36b 100644 --- a/pkg/api/build.go +++ b/pkg/api/build.go @@ -18,6 +18,11 @@ import ( func createBuild(name, format, output string, w http.ResponseWriter, r *http.Request) { log.Debugf("create build, name: %s, format: %s, output: %s", name, format, output) body, _ := ioutil.ReadAll(r.Body) + imgConfig, err := image.NewConfig(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } buildDir, err := ioutil.TempDir("", "linuxkit") if err != nil { @@ -27,22 +32,22 @@ func createBuild(name, format, output string, w http.ResponseWriter, r *http.Req if err := linuxkit.Build(name, body, []string{format}, buildDir); err != nil { if linuxkit.IsInvalidConfiguration(err) { - http.Error(w, err.Error(), 400) + http.Error(w, err.Error(), http.StatusBadRequest) } else if linuxkit.IsBuildFailed(err) { - http.Error(w, err.Error(), 503) + http.Error(w, err.Error(), http.StatusServiceUnavailable) } else { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) } return } - if err := writeResponse(buildDir, name, format, output, w); err != nil { + if err := writeResponse(buildDir, name, imgConfig, format, output, w); err != nil { log.Debugf("Writing response caused error: %s", err) http.Error(w, err.Error(), 500) } } -func writeResponse(buildDir, name, format, output string, w io.Writer) error { +func writeResponse(buildDir, name string, config *image.Config, format, output string, w io.Writer) error { log.Debugf("write response buildDir: %s, name: %s, format: %s, output: %s", buildDir, name, format, output) switch format { case "rpi3": @@ -65,7 +70,7 @@ func writeResponse(buildDir, name, format, output string, w io.Writer) error { return errors.Wrap(err, "Error while unpacking rpi3 package") } - if err := image.Build(tempDir, w); err != nil { + if err := image.Build(tempDir, config.Image.Partitions, w); err != nil { return errors.Wrap(err, "Failed to build img file") } default: diff --git a/pkg/image/builder.go b/pkg/image/builder.go index 3cde136..8f205c9 100644 --- a/pkg/image/builder.go +++ b/pkg/image/builder.go @@ -1,11 +1,11 @@ package image import ( - "fmt" "io" "io/ioutil" "os" "os/exec" + "strconv" "strings" "github.com/ernoaapa/linuxkit-server/pkg/utils" @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" ) -func Build(sourceDir string, w io.Writer) error { +func Build(sourceDir string, partitions []Partition, w io.Writer) error { tmpfile, err := ioutil.TempFile("", "img") if err != nil { return errors.Wrapf(err, "Failed to create temporary file") @@ -21,7 +21,7 @@ func Build(sourceDir string, w io.Writer) error { defer tmpfile.Close() defer os.Remove(tmpfile.Name()) - if err := buildImage(sourceDir, tmpfile.Name()); err != nil { + if err := buildImage(sourceDir, partitions, tmpfile.Name()); err != nil { return err } @@ -32,11 +32,20 @@ func Build(sourceDir string, w io.Writer) error { return nil } -func buildImage(sourceDir, filename string) error { - if err := createZeroFile(filename, 1024*1024*100); err != nil { +func buildImage(sourceDir string, partitions []Partition, filename string) error { + bootSize, err := utils.GetDirSize(sourceDir) + if err != nil { + return errors.Wrapf(err, "Failed to resolve image source size") + } + + if err := validatePartitionTable(partitions, uint64(bootSize)); err != nil { return err } - if err := createFat32Partition(filename); err != nil { + + if err := createZeroFile(filename, getTotalSize(partitions)); err != nil { + return err + } + if err := createPartitions(filename, partitions); err != nil { return err } @@ -50,20 +59,32 @@ func buildImage(sourceDir, filename string) error { return err } - var device string - switch l := len(devices); l { - case 0: - return fmt.Errorf("%s don't have any paritions. There must be single partition", filename) - case 1: - device = devices[0] - default: - return fmt.Errorf("%s contain multiple paritions, but we support only single partition img files", filename) - } + for i, device := range devices { + partition := partitions[i] + + switch partition.FsType { + case "fat32": + if err := formatFat32(device); err != nil { + return errors.Wrapf(err, "Failed to format device %s as Fat32", device) + } + + case "ext4": + if err := formatExt4(device); err != nil { + return errors.Wrapf(err, "Failed to format device %s as ext4", device) + } + } - if err := formatFat32(device); err != nil { - return errors.Wrapf(err, "Failed to format device %s as Fat32", device) + if partition.Boot { + if err := writeBootPartition(device, sourceDir); err != nil { + return err + } + } } + return nil +} + +func writeBootPartition(device, sourceDir string) error { buildDir, err := ioutil.TempDir("/mnt", "") if err != nil { return errors.Wrapf(err, "Failed to create temporary build directory") @@ -71,7 +92,7 @@ func buildImage(sourceDir, filename string) error { defer os.RemoveAll(buildDir) if err := mountDevice(device, buildDir); err != nil { - return errors.Wrapf(err, "Failed to mount device %s to dir %s", device, buildDir) + return errors.Wrapf(err, "Failed to mount root partition %s to dir %s", device, buildDir) } defer unmountDevice(buildDir) @@ -82,7 +103,7 @@ func buildImage(sourceDir, filename string) error { return nil } -func createZeroFile(path string, size int64) error { +func createZeroFile(path string, size uint64) error { log.Debugf("Create %d bytes empty zero file to %s", size, path) f, err := os.Create(path) if err != nil { @@ -90,22 +111,23 @@ func createZeroFile(path string, size int64) error { } defer f.Close() - if err := f.Truncate(size); err != nil { + if err := f.Truncate(int64(size)); err != nil { return errors.Wrapf(err, "Failed to fill file %s to size %d", path, size) } return nil } -func createFat32Partition(path string) error { - log.Debugf("Create Fat32 partition to %s", path) +func createPartitions(path string, table []Partition) error { + log.Debugf("Create partitions to %s", path) if err := runParted(path, "mklabel", "msdos"); err != nil { return errors.Wrapf(err, "Failed to execute 'parted mklabel' to %s", path) } - if err := runParted("--script", "--align=opt", path, "mkpart", "primary", "fat32", "2048s", "100%"); err != nil { - return errors.Wrapf(err, "Failed to execute 'parted mkpart' to %s", path) - } - if err := runParted(path, "set", "1", "boot", "on"); err != nil { - return errors.Wrapf(err, "Failed to 'parted set boot on' to %s", path) + for i, partition := range table { + args := []string{"--script", "--align=opt", path, "mkpart", "primary", partition.FsType, strconv.FormatUint(partition.Start, 10) + "B", strconv.FormatUint(partition.Start+partition.Size, 10) + "B"} + if err := runParted(args...); err != nil { + log.Debugf("Failed to execute (parted %s): %s", strings.Join(args, " "), err) + return errors.Wrapf(err, "Failed to create %d/%d partition to %s", i, len(table), path) + } } return nil } @@ -123,6 +145,13 @@ func formatFat32(device string) error { return cmd.Run() } +func formatExt4(device string) error { + log.Debugf("Format device %s", device) + cmd := exec.Command("mkfs.ext4", device) + cmd.Stdout = os.Stdout + return cmd.Run() +} + func mountDevice(device, path string) error { log.Debugf("Mount device %s to path %s", device, path) cmd := exec.Command("mount", device, path) diff --git a/pkg/image/config.go b/pkg/image/config.go new file mode 100644 index 0000000..da9cd59 --- /dev/null +++ b/pkg/image/config.go @@ -0,0 +1,42 @@ +package image + +import ( + "github.com/c2h5oh/datasize" + yaml "gopkg.in/yaml.v2" +) + +// Config includes extra fields what linuxkit-server supports in the Linuxkit config +type Config struct { + Image Image +} + +// Image output configuration +type Image struct { + Partitions []Partition +} + +// Partition contains information about disk partition +type Partition struct { + Boot bool `yaml:"boot"` + Start uint64 `yaml:"start"` + Size uint64 `yaml:"size"` + FsType string `yaml:"type"` +} + +// NewConfig parses a config file +func NewConfig(raw []byte) (*Config, error) { + var config = &Config{} + + err := yaml.Unmarshal(raw, config) + if err != nil { + return config, err + } + + if len(config.Image.Partitions) == 0 { + config.Image.Partitions = []Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + } + } + + return config, nil +} diff --git a/pkg/image/config_test.go b/pkg/image/config_test.go new file mode 100644 index 0000000..2a8ac5a --- /dev/null +++ b/pkg/image/config_test.go @@ -0,0 +1,35 @@ +package image + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewConfigWithNormalLinuxkitConfig(t *testing.T) { + _, err := NewConfig([]byte(` +kernel: + image: linuxkit/kernel:4.9.69 + cmdline: "console=tty0 console=ttyS0 console=ttyAMA0" +trust: + org: + - linuxkit`)) + assert.NoError(t, err) +} + +func TestNewConfigWithExtraFields(t *testing.T) { + config, err := NewConfig([]byte(` +image: + partitions: + - { start: 0, size: 104857600, boot: true, type: "fat32" } + - { start: 104857600, size: 209715200, type: "ext4" } +kernel: + image: linuxkit/kernel:4.9.69 + cmdline: "console=tty0 console=ttyS0 console=ttyAMA0" +trust: + org: + - linuxkit`)) + assert.NoError(t, err) + assert.Equal(t, 2, len(config.Image.Partitions)) + assert.Equal(t, "fat32", config.Image.Partitions[0].FsType) +} diff --git a/pkg/image/partition.go b/pkg/image/partition.go new file mode 100644 index 0000000..927b6f3 --- /dev/null +++ b/pkg/image/partition.go @@ -0,0 +1,73 @@ +package image + +import ( + "errors" + "fmt" + "strings" +) + +// The default partition sector size +var sectorSize = uint64(512) +var startOffset = sectorSize * 2048 +var endOffset = sectorSize + +var supportedTypes = []string{"fat32", "ext4"} + +func validatePartitionTable(table []Partition, bootSize uint64) error { + if len(table) > 4 { + return errors.New("Only max 4 partitions are supported") + } + + for _, partition := range table { + if !contains(supportedTypes, partition.FsType) { + return fmt.Errorf("%s is unsupported partition file system type. Supported types are %s", partition.FsType, strings.Join(supportedTypes, ",")) + } + } + + bootPartitions := getBootPartitions(table) + if len(bootPartitions) == 0 { + return errors.New("Partition table must have at least one boot partition") + } else if len(bootPartitions) > 1 { + return errors.New("Partition table can contain only one partition with boot=true") + } + + boot := bootPartitions[0] + + if boot.FsType != "fat32" { + return errors.New("Boot partition file system type must be fat32") + } + + if boot.Size < bootSize { + return errors.New("Boot partition is not large enough for the boot files") + } + + return nil +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func getBootPartitions(table []Partition) (result []Partition) { + for _, p := range table { + if p.Boot { + result = append(result, p) + } + } + return result +} + +func getTotalSize(table []Partition) uint64 { + var last Partition + for _, p := range table { + if last.Start < p.Start { + last = p + } + } + return last.Start + last.Size + sectorSize +} diff --git a/pkg/image/partition_test.go b/pkg/image/partition_test.go new file mode 100644 index 0000000..ec4c877 --- /dev/null +++ b/pkg/image/partition_test.go @@ -0,0 +1,91 @@ +package image + +import ( + "fmt" + "testing" + + "github.com/c2h5oh/datasize" + "github.com/stretchr/testify/assert" +) + +func TestValidatePartitionTableBootFlag(t *testing.T) { + assert.NoError(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }, uint64(datasize.MB*50)), "should be valid if single boot partition") + + assert.Error(t, validatePartitionTable([]Partition{}, 0), "should return error if no boot partition") + assert.Error(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }, uint64(datasize.MB*50)), "should return error if have two boot partitions") +} + +func TestValidatePartitionTableBootType(t *testing.T) { + assert.NoError(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }, uint64(datasize.MB*50)), "should be valid if boot file system type is fat32") + + assert.Error(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "ext4"}, + }, uint64(datasize.MB*50)), "should be invalid if boot file system type is not fat32") +} + +func TestValidatePartitionTableBootSize(t *testing.T) { + assert.NoError(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }, uint64(datasize.MB*50)), "should be valid if boot filesystem is large enough for boot files") + + assert.Error(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }, uint64(datasize.MB*200)), "should be invalid if boot filesystem is not large enough for boot files") +} + +func TestValidatePartitionTableAllowMaxFourPrimaryPartitions(t *testing.T) { + assert.NoError(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + {Start: startOffset + uint64(datasize.MB*200), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + {Start: startOffset + uint64(datasize.MB*300), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + }, uint64(datasize.MB*50)), "should be valid if four partitions") + + assert.Error(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + {Start: startOffset + uint64(datasize.MB*200), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + {Start: startOffset + uint64(datasize.MB*300), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + {Start: startOffset + uint64(datasize.MB*400), Size: uint64(datasize.MB * 100), FsType: "ext4"}, + }, uint64(datasize.MB*50)), "should be valid if over four partitions") +} + +func TestValidatePartitionFsTypes(t *testing.T) { + for _, fsType := range []string{"fat32", "ext4"} { + assert.NoError(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), FsType: fsType}, + }, uint64(datasize.MB*50)), fmt.Sprintf("should support %s file system type partitions", fsType)) + } + + for _, fsType := range []string{"fat16", "ext2"} { + assert.Error(t, validatePartitionTable([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), FsType: fsType}, + }, uint64(datasize.MB*50)), fmt.Sprintf("should not support %s file system type partitions", fsType)) + } +} + +func TestGetTotalSize(t *testing.T) { + assert.Equal(t, + startOffset+uint64(datasize.MB*100)+endOffset, + getTotalSize([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + }), + "Should include start offset into total size") + + assert.Equal(t, + startOffset+uint64(datasize.MB*100)+uint64(datasize.MB*100)+endOffset, + getTotalSize([]Partition{ + {Start: startOffset, Size: uint64(datasize.MB * 100), Boot: true, FsType: "fat32"}, + {Start: startOffset + uint64(datasize.MB*100), Size: uint64(datasize.MB * 100), Boot: true, FsType: "ext4"}, + }), + "Should sum all partitions to total size") +} diff --git a/pkg/utils/fs.go b/pkg/utils/fs.go new file mode 100644 index 0000000..cb248f2 --- /dev/null +++ b/pkg/utils/fs.go @@ -0,0 +1,24 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" +) + +func visit(path string, f os.FileInfo, err error) error { + fmt.Printf("Visited: %s\n", path) + return nil +} + +// GetDirSize traverses directory and returns the total size of files in bytes +func GetDirSize(root string) (int64, error) { + var size int64 + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} diff --git a/pkg/utils/fs_test.go b/pkg/utils/fs_test.go new file mode 100644 index 0000000..2794e32 --- /dev/null +++ b/pkg/utils/fs_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetDirSize(t *testing.T) { + dir, err := os.Getwd() + assert.NoError(t, err, "Failed to resolve current working directory for test") + + size, err := GetDirSize(dir) + assert.NoError(t, err) + assert.True(t, size > 0) +} diff --git a/vendor.conf b/vendor.conf index b64c928..985d181 100644 --- a/vendor.conf +++ b/vendor.conf @@ -28,4 +28,5 @@ github.com/pmezard/go-difflib v1.0.0 github.com/dockpit/dirtar db576360df5e1e1c8fc843abe0be18df3969cce7 https://github.com/ernoaapa/dirtar github.com/stretchr/testify v1.1.4 github.com/davecgh/go-spew v1.1.0 -github.com/gorilla/handlers v1.3.0 \ No newline at end of file +github.com/gorilla/handlers v1.3.0 +github.com/c2h5oh/datasize 4eba002a5eaea69cf8d235a388fc6b65ae68d2dd \ No newline at end of file diff --git a/vendor/github.com/c2h5oh/datasize/LICENSE b/vendor/github.com/c2h5oh/datasize/LICENSE new file mode 100644 index 0000000..f2ba916 --- /dev/null +++ b/vendor/github.com/c2h5oh/datasize/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Maciej Lisiewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/c2h5oh/datasize/README.md b/vendor/github.com/c2h5oh/datasize/README.md new file mode 100644 index 0000000..d21f136 --- /dev/null +++ b/vendor/github.com/c2h5oh/datasize/README.md @@ -0,0 +1,66 @@ +# datasize [![Build Status](https://travis-ci.org/c2h5oh/datasize.svg?branch=master)](https://travis-ci.org/c2h5oh/datasize) + +Golang helpers for data sizes + + +### Constants +Just like `time` package provides `time.Second`, `time.Day` constants `datasize` provides: +* `datasize.B` 1 byte +* `datasize.KB` 1 kilobyte +* `datasize.MB` 1 megabyte +* `datasize.GB` 1 gigabyte +* `datasize.TB` 1 terabyte +* `datasize.PB` 1 petabyte +* `datasize.EB` 1 exabyte + +### Helpers +Just like `time` package provides `duration.Nanoseconds() uint64 `, `duration.Hours() float64` helpers `datasize` has +* `ByteSize.Bytes() uint64` +* `ByteSize.Kilobytes() float4` +* `ByteSize.Megabytes() float64` +* `ByteSize.Gigabytes() float64` +* `ByteSize.Terabytes() float64` +* `ByteSize.Petebytes() float64` +* `ByteSize.Exabytes() float64` + +Warning: see limitations at the end of this document about a possible precission loss + +### Parsing strings +`datasize.ByteSize` implements `TextUnmarshaler` interface and will automatically parse human readable strings into correct values where it is used: +* `"10 MB"` -> `10* datasize.MB` +* `"10240 g"` -> `10 * datasize.TB` +* `"2000"` -> `2000 * datasize.B` +* `"1tB"` -> `datasize.TB` +* `"5 peta"` -> `5 * datasize.PB` +* `"28 kilobytes"` -> `28 * datasize.KB` +* `"1 gigabyte"` -> `1 * datasize.GB` + +You can also do it manually: +```go +var v datasize.ByteSize +err := v.UnmarshalText([]byte("100 mb")) +``` + +### Printing +`Bytesize.String()` uses largest unit allowing an integer value: + * `(102400 * datasize.MB).String()` -> `"100GB"` + * `(datasize.MB + datasize.KB).String()` -> `"1025KB"` + +Use `%d` format string to get value in bytes without a unit + +### JSON and other encoding +Both `TextMarshaler` and `TextUnmarshaler` interfaces are implemented - JSON will just work. Other encoders will work provided they use those interfaces. + +### Human readable +`ByteSize.HumanReadable()` or `ByteSize.HR()` returns a string with 1-3 digits, followed by 1 decimal place, a space and unit big enough to get 1-3 digits + + * `(102400 * datasize.MB).String()` -> `"100.0 GB"` + * `(datasize.MB + 512 * datasize.KB).String()` -> `"1.5 MB"` + +### Limitations +* The underlying data type for `data.ByteSize` is `uint64`, so values outside of 0 to 2^64-1 range will overflow +* size helper functions (like `ByteSize.Kilobytes()`) return `float64`, which can't represent all possible values of `uint64` accurately: + * if the returned value is supposed to have no fraction (ie `(10 * datasize.MB).Kilobytes()`) accuracy loss happens when value is more than 2^53 larger than unit: `.Kilobytes()` over 8 petabytes, `.Megabytes()` over 8 exabytes + * if the returned value is supposed to have a fraction (ie `(datasize.PB + datasize.B).Megabytes()`) in addition to the above note accuracy loss may occur in fractional part too - larger integer part leaves fewer bytes to store fractional part, the smaller the remainder vs unit the move bytes are required to store the fractional part +* Parsing a string with `Mb`, `Tb`, etc units will return a syntax error, because capital followed by lower case is commonly used for bits, not bytes +* Parsing a string with value exceeding 2^64-1 bytes will return 2^64-1 and an out of range error diff --git a/vendor/github.com/c2h5oh/datasize/datasize.go b/vendor/github.com/c2h5oh/datasize/datasize.go new file mode 100644 index 0000000..6754788 --- /dev/null +++ b/vendor/github.com/c2h5oh/datasize/datasize.go @@ -0,0 +1,217 @@ +package datasize + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +type ByteSize uint64 + +const ( + B ByteSize = 1 + KB = B << 10 + MB = KB << 10 + GB = MB << 10 + TB = GB << 10 + PB = TB << 10 + EB = PB << 10 + + fnUnmarshalText string = "UnmarshalText" + maxUint64 uint64 = (1 << 64) - 1 + cutoff uint64 = maxUint64 / 10 +) + +var ErrBits = errors.New("unit with capital unit prefix and lower case unit (b) - bits, not bytes ") + +func (b ByteSize) Bytes() uint64 { + return uint64(b) +} + +func (b ByteSize) KBytes() float64 { + v := b / KB + r := b % KB + return float64(v) + float64(r)/float64(KB) +} + +func (b ByteSize) MBytes() float64 { + v := b / MB + r := b % MB + return float64(v) + float64(r)/float64(MB) +} + +func (b ByteSize) GBytes() float64 { + v := b / GB + r := b % GB + return float64(v) + float64(r)/float64(GB) +} + +func (b ByteSize) TBytes() float64 { + v := b / TB + r := b % TB + return float64(v) + float64(r)/float64(TB) +} + +func (b ByteSize) PBytes() float64 { + v := b / PB + r := b % PB + return float64(v) + float64(r)/float64(PB) +} + +func (b ByteSize) EBytes() float64 { + v := b / EB + r := b % EB + return float64(v) + float64(r)/float64(EB) +} + +func (b ByteSize) String() string { + switch { + case b == 0: + return fmt.Sprint("0B") + case b%EB == 0: + return fmt.Sprintf("%dEB", b/EB) + case b%PB == 0: + return fmt.Sprintf("%dPB", b/PB) + case b%TB == 0: + return fmt.Sprintf("%dTB", b/TB) + case b%GB == 0: + return fmt.Sprintf("%dGB", b/GB) + case b%MB == 0: + return fmt.Sprintf("%dMB", b/MB) + case b%KB == 0: + return fmt.Sprintf("%dKB", b/KB) + default: + return fmt.Sprintf("%dB", b) + } +} + +func (b ByteSize) HR() string { + return b.HumanReadable() +} + +func (b ByteSize) HumanReadable() string { + switch { + case b > EB: + return fmt.Sprintf("%.1f EB", b.EBytes()) + case b > PB: + return fmt.Sprintf("%.1f PB", b.PBytes()) + case b > TB: + return fmt.Sprintf("%.1f TB", b.TBytes()) + case b > GB: + return fmt.Sprintf("%.1f GB", b.GBytes()) + case b > MB: + return fmt.Sprintf("%.1f MB", b.MBytes()) + case b > KB: + return fmt.Sprintf("%.1f KB", b.KBytes()) + default: + return fmt.Sprintf("%d B", b) + } +} + +func (b ByteSize) MarshalText() ([]byte, error) { + return []byte(b.String()), nil +} + +func (b *ByteSize) UnmarshalText(t []byte) error { + var val uint64 + var unit string + + // copy for error message + t0 := t + + var c byte + var i int + +ParseLoop: + for i < len(t) { + c = t[i] + switch { + case '0' <= c && c <= '9': + if val > cutoff { + goto Overflow + } + + c = c - '0' + val *= 10 + + if val > val+uint64(c) { + // val+v overflows + goto Overflow + } + val += uint64(c) + i++ + + default: + if i == 0 { + goto SyntaxError + } + break ParseLoop + } + } + + unit = strings.TrimSpace(string(t[i:])) + switch unit { + case "Kb", "Mb", "Gb", "Tb", "Pb", "Eb": + goto BitsError + } + unit = strings.ToLower(unit) + switch unit { + case "", "b", "byte": + // do nothing - already in bytes + + case "k", "kb", "kilo", "kilobyte", "kilobytes": + if val > maxUint64/uint64(KB) { + goto Overflow + } + val *= uint64(KB) + + case "m", "mb", "mega", "megabyte", "megabytes": + if val > maxUint64/uint64(MB) { + goto Overflow + } + val *= uint64(MB) + + case "g", "gb", "giga", "gigabyte", "gigabytes": + if val > maxUint64/uint64(GB) { + goto Overflow + } + val *= uint64(GB) + + case "t", "tb", "tera", "terabyte", "terabytes": + if val > maxUint64/uint64(TB) { + goto Overflow + } + val *= uint64(TB) + + case "p", "pb", "peta", "petabyte", "petabytes": + if val > maxUint64/uint64(PB) { + goto Overflow + } + val *= uint64(PB) + + case "E", "EB", "e", "eb", "eB": + if val > maxUint64/uint64(EB) { + goto Overflow + } + val *= uint64(EB) + + default: + goto SyntaxError + } + + *b = ByteSize(val) + return nil + +Overflow: + *b = ByteSize(maxUint64) + return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrRange} + +SyntaxError: + *b = 0 + return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrSyntax} + +BitsError: + *b = 0 + return &strconv.NumError{fnUnmarshalText, string(t0), ErrBits} +} diff --git a/vendor/github.com/moby/tool/src/moby/schema.go b/vendor/github.com/moby/tool/src/moby/schema.go index 9379098..4e57a38 100644 --- a/vendor/github.com/moby/tool/src/moby/schema.go +++ b/vendor/github.com/moby/tool/src/moby/schema.go @@ -4,7 +4,7 @@ var schema = string(` { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Moby Config", - "additionalProperties": false, + "additionalProperties": true, "definitions": { "kernel": { "type": "object",