blob: 65b1fbc415fc6f68516bde42a510494cacbccc39 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// make-fuchsia-vol is a temporarily useful script that provisions Fuchsia
// volumes based on paths provided.
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"go.fuchsia.dev/fuchsia/garnet/go/src/thinfs/block/file"
"go.fuchsia.dev/fuchsia/garnet/go/src/thinfs/fs"
"go.fuchsia.dev/fuchsia/garnet/go/src/thinfs/fs/msdosfs"
"go.fuchsia.dev/fuchsia/garnet/go/src/thinfs/gpt"
"go.fuchsia.dev/fuchsia/garnet/go/src/thinfs/mbr"
)
var (
verbose = flag.Bool("verbose", false, "enable verbose logging")
fuchsiaBuildDir = flag.String("fuchsia-build-dir", os.Getenv("FUCHSIA_BUILD_DIR"), "fuchsia build dir")
bootloader = flag.String("bootloader", "", "path to bootx64.efi")
zbi = flag.String("zbi", "", "path to zbi (default: zircon-a from image manifests)")
cmdline = flag.String("cmdline", "", "path to command line file (if exists)")
zedboot = flag.String("zedboot", "", "path to zedboot.zbi (default: zircon-r from image manifests)")
ramdiskOnly = flag.Bool("ramdisk-only", false, "ramdisk-only mode - only write an ESP partition")
blob = flag.String("blob", "", "path to blob partition image (not used with ramdisk)")
data = flag.String("data", "", "path to data partition image (not used with ramdisk)")
abr = flag.Bool("abr", true, "add Zircon-{A,B,R} partitions")
zirconA = flag.String("zirconA", "", "path to partition image for Zircon-A (default: from -zbi)")
vbmetaA = flag.String("vbmetaA", "", "path to partition image for Vbmeta-A")
zirconB = flag.String("zirconB", "", "path to partition image for Zircon-B (default: from -zbi)")
vbmetaB = flag.String("vbmetaB", "", "path to partition image for Vbmeta-B")
zirconR = flag.String("zirconR", "", "path to partition image for Zircon-R (default: zircon-r from image manifests)")
vbmetaR = flag.String("vbmetaR", "", "path to partition image for Vbmeta-R")
abrSize = flag.Int64("abr-size", 256*1024*1024, "Kernel partition size for A/B/R")
vbmetaSize = flag.Int64("vbmeta-size", 8*1024, "partition size for vbmeta A/B/R")
abrBoot = flag.String("abr-boot", "a", "A/B/R partition to boot by default")
blockSize = flag.Int64("block-size", 0, "the block size of the target disk (0 means detect)")
physicalBlockSize = flag.Int64("physical-block-size", 0, "the physical block size of the target disk (0 means detect)")
optimalTransferSize = flag.Int64("optimal-transfer-size", 0, "the optimal transfer size of the target disk (0 means unknown/unused)")
efiSize = flag.Int64("efi-size", 63*1024*1024, "efi partition size in bytes")
fvmSize = flag.Int64("fvm-size", 0, "fvm partition size in bytes (0 means `fill`)")
resize = flag.Int64("resize", 0, "create or resize the image to this size in bytes")
)
// imageManifest is basename of the image manifest file.
const imageManifest = "images.json"
type imageManifestEntry struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
// imagePaths contains the default image paths that are produced by a build manifest, populated by tryLoadManifests.
var imagePaths = map[string]string{}
// getImage fetches an image by name or exits fatally
func getImage(name string) string {
if path, ok := imagePaths[name]; ok {
return path
}
log.Fatalf("Missing image path: %q cannot continue", name)
return ""
}
func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s disk-path\n", filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
}
func tryLoadManifests() {
if *fuchsiaBuildDir == "" {
return
}
f, err := os.Open(filepath.Join(*fuchsiaBuildDir, imageManifest))
if err != nil {
log.Printf("warning: failed to load %s: %v", imageManifest, err)
return
}
defer f.Close()
var entries []imageManifestEntry
if err := json.NewDecoder(f).Decode(&entries); err != nil {
log.Printf("warning: failed to load %s: %v", imageManifest, err)
return
}
for _, image := range entries {
imagePaths[image.Type+"_"+image.Name] = image.Path
}
}
func needFuchsiaBuildDir() {
if *fuchsiaBuildDir == "" {
log.Fatalf("either pass -fuchsia-build-dir or set $FUCHSIA_BUILD_DIR")
}
}
func main() {
flag.Parse()
tryLoadManifests()
if *bootloader == "" {
needFuchsiaBuildDir()
*bootloader = filepath.Join(*fuchsiaBuildDir, "efi_x64/bootx64.efi")
}
if _, err := os.Stat(*bootloader); err != nil {
log.Fatalf("cannot read %q: %s", *bootloader, err)
}
if *zbi == "" {
needFuchsiaBuildDir()
*zbi = filepath.Join(*fuchsiaBuildDir, getImage("zbi_zircon-a"))
}
if _, err := os.Stat(*zbi); err != nil {
log.Fatalf("cannot read %q: %s", *zbi, err)
}
if *zedboot == "" {
needFuchsiaBuildDir()
*zedboot = filepath.Join(*fuchsiaBuildDir, getImage("zbi_zircon-r"))
}
if _, err := os.Stat(*zedboot); err != nil {
log.Fatalf("cannot read %q: %s", *zedboot, err)
}
if *cmdline == "" {
needFuchsiaBuildDir()
p := filepath.Join(*fuchsiaBuildDir, "cmdline")
if _, err := os.Stat(p); err == nil {
*cmdline = p
}
} else {
if _, err := os.Stat(*cmdline); err != nil {
log.Fatal(err)
}
}
bootMode := BOOT_A
if *abr {
if *zirconA == "" {
needFuchsiaBuildDir()
*zirconA = *zbi
}
if *vbmetaA == "" {
*vbmetaA = filepath.Join(*fuchsiaBuildDir, getImage("vbmeta_zircon-a"))
}
if *zirconB == "" {
*zirconB = *zbi
}
if *vbmetaB == "" {
*vbmetaB = filepath.Join(*fuchsiaBuildDir, getImage("vbmeta_zircon-a"))
}
if *zirconR == "" {
*zirconR = filepath.Join(*fuchsiaBuildDir, getImage("zbi_zircon-r"))
}
if *vbmetaR == "" {
*vbmetaR = filepath.Join(*fuchsiaBuildDir, getImage("vbmeta_zircon-r"))
}
switch strings.ToLower(*abrBoot) {
case "a":
bootMode = BOOT_A
case "b":
bootMode = BOOT_B
case "r":
bootMode = BOOT_RECOVERY
default:
log.Fatalf("Invalid -abr-boot passed: expected 'a', 'b', or 'r'.")
}
}
if !*ramdiskOnly {
if *blob == "" {
needFuchsiaBuildDir()
*blob = filepath.Join(*fuchsiaBuildDir, getImage("blk_blob"))
}
if *data == "" {
needFuchsiaBuildDir()
*data = filepath.Join(*fuchsiaBuildDir, getImage("blk_data"))
}
if _, err := os.Stat(*blob); err != nil {
log.Fatalf("Blob image error: %s\nEither provide a blob image, or pass -ramdisk-only", err)
}
if _, err := os.Stat(*data); err != nil {
log.Fatalf("Data image error: %s\nEither provide a data image, or pass -ramdisk-only", err)
}
}
if len(flag.Args()) != 1 {
flag.Usage()
os.Exit(1)
}
disk, err := filepath.Abs(flag.Args()[0])
if err != nil {
log.Fatal(err)
}
if *resize != 0 {
s, err := os.Stat(disk)
if err == nil {
if s.Size() != *resize {
if *verbose {
log.Printf("Resizing %q from %d to %d", disk, s.Size(), *resize)
}
if err := os.Truncate(disk, *resize); err != nil {
log.Fatalf("failed to truncate %q to %d: %s", disk, *resize, err)
}
}
} else if os.IsNotExist(err) {
if *verbose {
log.Printf("Creating %q", disk)
}
f, err := os.Create(disk)
if err != nil {
log.Fatalf("failed to create %q: %s", disk, err)
}
if err := f.Truncate(*resize); err != nil {
log.Fatalf("failed to truncate %q to %d: %s", disk, *resize, err)
}
f.Close()
} else {
log.Fatal(err)
}
} else {
if _, err := os.Stat(disk); err != nil {
log.Fatalf("cannot read %q: %s\n", disk, err)
}
}
f, err := os.Open(disk)
if err != nil {
log.Fatal(err)
}
if *blockSize == 0 {
lbs, err := gpt.GetLogicalBlockSize(f)
if err != nil {
log.Printf("WARNING: could not detect logical block size: %s. Assuming %d\n", err, lbs)
}
*blockSize = int64(lbs)
}
if *physicalBlockSize == 0 {
pbs, err := gpt.GetPhysicalBlockSize(f)
if err != nil {
log.Printf("WARNING: could not detect physical block size: %s. Assuming %d\n", err, pbs)
}
*physicalBlockSize = int64(pbs)
}
if *physicalBlockSize < 4096 && runtime.GOOS == "darwin" {
// OSX is not reliably returning correct values for USB sticks, unclear why
*physicalBlockSize = 4096
}
var (
logical = uint64(*blockSize)
physical = uint64(*physicalBlockSize)
optimal = uint64(*optimalTransferSize)
diskSize uint64
)
// ignore the error here as it may be an image file...
diskSize, _ = gpt.GetDiskSize(f)
if diskSize == 0 {
s, err := os.Stat(disk)
if err != nil {
log.Fatalf("could not stat %q: %s\n", disk, err)
}
diskSize = uint64(s.Size())
}
if diskSize == 0 {
log.Fatalf("could not determine size of %q", disk)
}
// Note: this isn't entirely correct, as it doesn't take into account padding.
// Consider adding a real API for this in the GPT lib.
minGPTSize := int64((gpt.MinPartitionEntryArraySize + gpt.HeaderSize) * 2)
if uint64(*efiSize+minGPTSize) > diskSize {
log.Fatalf("%q is not large enough for the partition layout\n", disk)
}
if *verbose {
log.Printf("Disk: %s", disk)
log.Printf("Disk size: %d", diskSize)
log.Printf("Block Size: %d", logical)
log.Printf("Physical block size: %d", physical)
log.Printf("Optimal transfer size: %d", optimal)
}
g, err := gpt.ReadGPT(f, logical, diskSize)
if err != nil {
log.Fatal(err)
}
lbaSize := diskSize / logical
g.MBR = mbr.NewProtectiveMBR(lbaSize)
g.Primary.Partitions = []gpt.PartitionEntry{}
g.Update(logical, physical, optimal, diskSize) // for the firstusablelba
end := g.Primary.FirstUsableLBA
var efiStart uint64
efiStart, end = optimalBlockAlign(end, uint64(*efiSize), logical, physical, optimal)
// compute the size of the fat geometry that fits within the well-aligned GPT
// partition that was computed above.
*efiSize = fitFAT(int64((end-1)-efiStart) * int64(logical))
// efiEnd is the last sector of viable fat geometry, which may be different
// from end, which is the last sector of the gpt partition.
efiEnd := efiStart + (uint64(*efiSize) / logical) - 1
if *verbose {
log.Printf("EFI START: %d", efiStart)
log.Printf("EFI END: %d", efiEnd)
log.Printf("EFI LB SIZE: %d", efiEnd-efiStart+1)
}
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDEFI,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("efi-system"),
StartingLBA: efiStart,
EndingLBA: end,
})
var startingCHS [3]byte
startingCHS[0] = byte(efiStart / (16 * 63))
startingCHS[1] = byte((efiStart / 63) % 16)
startingCHS[2] = byte((efiStart % 63) + 1)
var endingCHS [3]byte
endingCHS[0] = byte(efiEnd / (16 * 63))
endingCHS[1] = byte((efiEnd / 63) % 16)
endingCHS[2] = byte((efiEnd % 63) + 1)
// Install a "hybrid MBR" hack for the case of bios bootloaders that might
// need it (e.g. rpi's binary blob that's stuck in MBR land).
g.MBR.PartitionRecord[2] = mbr.PartitionRecord{
BootIndicator: 0x80,
StartingCHS: startingCHS,
EndingCHS: endingCHS,
OSType: mbr.FAT32,
StartingLBA: uint32(efiStart),
SizeInLBA: uint32(efiEnd),
}
var aStart, bStart, rStart uint64
var vbmetaAStart, vbmetaBStart, vbmetaRStart, miscStart uint64
if *abr {
aStart, end = optimalBlockAlign(end, uint64(*abrSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaZirconA,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("ZIRCON-A"),
StartingLBA: aStart,
EndingLBA: end,
})
vbmetaAStart, end = optimalBlockAlign(end, uint64(*vbmetaSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaVbmetaA,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("VBMETA_A"),
StartingLBA: vbmetaAStart,
EndingLBA: end,
})
bStart, end = optimalBlockAlign(end, uint64(*abrSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaZirconB,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("ZIRCON-B"),
StartingLBA: bStart,
EndingLBA: end,
})
vbmetaBStart, end = optimalBlockAlign(end, uint64(*vbmetaSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaVbmetaB,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("VBMETA_B"),
StartingLBA: vbmetaBStart,
EndingLBA: end,
})
rStart, end = optimalBlockAlign(end, uint64(*abrSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaZirconR,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("ZIRCON-R"),
StartingLBA: rStart,
EndingLBA: end,
})
vbmetaRStart, end = optimalBlockAlign(end, uint64(*vbmetaSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaVbmetaR,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("VBMETA_R"),
StartingLBA: vbmetaRStart,
EndingLBA: end,
})
miscStart, end = optimalBlockAlign(end, uint64(*vbmetaSize), logical, physical, optimal)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaMisc,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("MISC"),
StartingLBA: miscStart,
EndingLBA: end,
})
}
var fvmStart uint64
fvmStart, end = optimalBlockAlign(end+1, uint64(*fvmSize), logical, physical, optimal)
if !*ramdiskOnly {
if *fvmSize == 0 {
end = g.Primary.LastUsableLBA
}
*fvmSize = int64((end - fvmStart) * logical)
g.Primary.Partitions = append(g.Primary.Partitions, gpt.PartitionEntry{
PartitionTypeGUID: gpt.GUIDFuchsiaFVM,
UniquePartitionGUID: gpt.NewRandomGUID(),
PartitionName: gpt.NewPartitionName("FVM"),
StartingLBA: fvmStart,
EndingLBA: end,
})
}
g.Update(logical, physical, optimal, diskSize)
if err := g.Validate(); err != nil {
log.Fatal(err)
}
if *verbose {
log.Printf("EFI size: %d", *efiSize)
if !*ramdiskOnly {
log.Printf("FVM size: %d", *fvmSize)
}
log.Printf("Writing GPT")
}
f, err = os.OpenFile(disk, os.O_RDWR, 0750)
if err != nil {
log.Fatal(err)
}
if _, err := g.WriteTo(f); err != nil {
log.Fatalf("error writing partition table: %s", err)
}
f.Sync()
aStart = aStart * logical
vbmetaAStart = vbmetaAStart * logical
bStart = bStart * logical
vbmetaBStart = vbmetaBStart * logical
rStart = rStart * logical
vbmetaRStart = vbmetaRStart * logical
miscStart = miscStart * logical
efiStart = efiStart * logical
fvmStart = fvmStart * logical
if *verbose {
log.Printf("Writing EFI partition and files")
}
cmd := exec.Command(fuchsiaTool("mkfs-msdosfs"),
"-@", strconv.FormatUint(efiStart, 10),
// XXX(raggi): mkfs-msdosfs offset gets subtracted by the tool for available
// size, so we have to add the offset back on to get the correct geometry.
"-S", strconv.FormatUint(uint64(*efiSize)+efiStart, 10),
"-F", "32",
"-L", "ESP",
"-O", "Fuchsia",
"-b", fmt.Sprintf("%d", logical),
disk,
)
if out, err := cmd.CombinedOutput(); err != nil {
log.Printf("mkfs-msdosfs failed:\n%s", out)
log.Fatal(err)
}
dev, err := file.NewRange(f, int64(logical), int64(efiStart), *efiSize)
if err != nil {
log.Fatal(err)
}
fatfs, err := msdosfs.New(disk, dev, fs.ReadWrite|fs.Force)
if err != nil {
log.Fatal(err)
}
root := fatfs.RootDirectory()
tf, err := ioutil.TempFile("", "gsetup-boot")
if err != nil {
log.Fatal(err)
}
tf.WriteString("efi\\boot\\bootx64.efi")
tf.Close()
defer os.Remove(tf.Name())
msCopyIn(root, tf.Name(), "EFI/Google/GSetup/Boot")
msCopyIn(root, *bootloader, "EFI/BOOT/bootx64.efi")
if !*abr {
msCopyIn(root, *zbi, "zircon.bin")
msCopyIn(root, *zedboot, "zedboot.bin")
}
if *cmdline != "" {
msCopyIn(root, *cmdline, "cmdline")
}
root.Sync()
if err := root.Close(); err != nil {
log.Fatal(err)
}
if err := fatfs.Close(); err != nil {
log.Fatal(err)
}
f.Sync()
if *abr {
if *verbose {
log.Print("Populating A/B/R and vbmeta partitions")
}
partitionCopy(f, int64(aStart), *abrSize, *zirconA)
partitionCopy(f, int64(vbmetaAStart), *vbmetaSize, *vbmetaA)
partitionCopy(f, int64(bStart), *abrSize, *zirconB)
partitionCopy(f, int64(vbmetaBStart), *vbmetaSize, *vbmetaB)
partitionCopy(f, int64(rStart), *abrSize, *zirconR)
partitionCopy(f, int64(vbmetaRStart), *vbmetaSize, *vbmetaR)
if _, err := f.Seek(int64(miscStart), os.SEEK_SET); err != nil {
log.Fatal(err)
}
if err := WriteAbr(bootMode, f); err != nil {
log.Fatal(err)
}
}
f.Sync()
if !*ramdiskOnly {
if *verbose {
log.Print("Populating FVM in GPT image")
}
fvm(disk, int64(fvmStart), *fvmSize, "create", "--blob", *blob, "--data", *data)
}
// Keep the file open so that OSX doesn't try to remount the disk while tools are working on it.
if err := f.Close(); err != nil {
log.Fatal(err)
}
if *verbose {
log.Printf("Done")
}
}
func partitionCopy(f *os.File, start, size int64, path string) {
input, err := os.Open(path)
if err != nil {
log.Fatalf("partition copy failed for input: %s: %s", path, err)
}
defer input.Close()
input_info, err := input.Stat()
if err != nil {
log.Fatalf("stat failed for input: %s: %s", path, err)
}
if input_info.Size() > size {
log.Printf("WARNING: %s is larger than the provided ABR size", path)
}
r := io.LimitReader(input, size)
if _, err := f.Seek(start, os.SEEK_SET); err != nil {
log.Fatalf("partition copy failed for input: %s: %s", path, err)
}
_, err = io.Copy(f, r)
if err != nil {
log.Fatalf("partition copy failed for input: %s: %s", path, err)
}
}
func fvm(disk string, offset, size int64, command string, args ...string) {
offs := strconv.FormatInt(offset, 10)
szs := strconv.FormatInt(size, 10)
argv := []string{disk, command, "--offset", offs, "--length", szs}
argv = append(argv, args...)
cmd := exec.Command(fuchsiaTool("fvm"), argv...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("fvm %s failed", argv)
}
}
// msCopyIn copies src from the host filesystem into dst under the given
// msdosfs root.
func msCopyIn(root fs.Directory, src, dst string) {
d := root
defer d.Sync()
dStack := []fs.Directory{}
defer func() {
for _, d := range dStack {
d.Sync()
d.Close()
}
}()
destdir := filepath.Dir(dst)
name := filepath.Base(dst)
for _, part := range strings.Split(destdir, "/") {
if part == "." {
continue
}
var err error
_, d, _, err = d.Open(part, fs.OpenFlagRead|fs.OpenFlagCreate|fs.OpenFlagDirectory)
if err != nil {
log.Fatalf("open/create %s: %#v %s", part, err, err)
}
d.Sync()
dStack = append(dStack, d)
}
to, _, _, err := d.Open(name, fs.OpenFlagWrite|fs.OpenFlagCreate|fs.OpenFlagFile)
if err != nil {
log.Fatalf("creating %s in msdosfs: %s", name, err)
}
defer to.Close()
from, err := os.Open(src)
if err != nil {
log.Fatal(err)
}
defer from.Close()
b := make([]byte, 4096)
for err == nil {
var n int
n, err = from.Read(b)
if n > 0 {
if _, err := to.Write(b[:n], 0, fs.WhenceFromCurrent); err != nil {
log.Fatalf("writing %s to msdosfs file: %s", name, err)
}
}
}
to.Sync()
if err != nil && err != io.EOF {
log.Fatal(err)
}
}
// optimalBlockAlign computes a start and end logical block address for a
// partition that starts at or after first (block address), of size byteSize,
// for a disk with logical, physical and optimal byte sizes. It returns the
// start and end block addresses.
func optimalBlockAlign(first, byteSize, logical, physical, optimal uint64) (start, end uint64) {
var alignTo = logical
if physical > alignTo {
alignTo = physical
}
if optimal > alignTo {
alignTo = optimal
}
lAlign := alignTo / logical
if d := first % lAlign; d != 0 {
start = first + lAlign - d
}
lSize := byteSize / logical
if byteSize%logical == 0 {
lSize++
}
end = start + lSize
return
}
func fuchsiaTool(name string) string {
var tool string
tool, _ = exec.LookPath(name)
if tool == "" {
needFuchsiaBuildDir()
tool, _ = exec.LookPath(filepath.Join(*fuchsiaBuildDir, "host_x64", name))
}
if tool == "" {
log.Fatalf("Could not find %q, you might need to build fuchsia", name)
}
return tool
}
// sectors per track is 63, and a sector is 512, so we must round to the nearest
// 32256.
const sizeAlignment = 32256
// N.B. fitFAT shrinks, not grows, as it intends to identify the nearest
// FAT-compatible geometry that fits inside of "total".
func fitFAT(total int64) int64 {
if d := total % sizeAlignment; d != 0 {
total = total - d
}
return total
}