~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
239 lines · 5.9 KB Raw History
package guestseed

import (
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"unicode/utf16"
)

const Label = "cidata"

const (
	sectorBytes   = 512
	rootEntries   = 512
	minClusters   = 4096
	maxClusters   = 65524
	maxPerCluster = 64
	fatDate       = 0x0021
)

type Seed struct {
	UserData      []byte
	MetaData      []byte
	NetworkConfig []byte
}

type document struct {
	name  string
	short string
	body  []byte
	first int
}

func (s Seed) documents() ([]*document, error) {
	if len(s.UserData) == 0 {
		return nil, fmt.Errorf("guestseed: the seed has no user-data, and user-data is the whole " +
			"contract a requester asked for: a guest booted from a seed without it comes up with " +
			"nothing installed and nothing reporting")
	}
	if len(s.MetaData) == 0 {
		return nil, fmt.Errorf("guestseed: the seed has no meta-data; NoCloud refuses a seed " +
			"without an instance-id, so the guest would run its datasource search to None instead")
	}
	docs := []*document{
		{name: "meta-data", short: "META-D~1   ", body: s.MetaData},
		{name: "user-data", short: "USER-D~1   ", body: s.UserData},
	}
	if len(s.NetworkConfig) > 0 {
		docs = append(docs, &document{name: "network-config", short: "NETWOR~1   ", body: s.NetworkConfig})
	}
	return docs, nil
}

func Image(seed Seed) ([]byte, error) {
	docs, err := seed.documents()
	if err != nil {
		return nil, err
	}
	perCluster := 1
	var clusters int
	for {
		need := 0
		for _, d := range docs {
			need += (len(d.body) + perCluster*sectorBytes - 1) / (perCluster * sectorBytes)
		}
		clusters = max(need+8, minClusters)
		if clusters <= maxClusters {
			break
		}
		perCluster *= 2
		if perCluster > maxPerCluster {
			return nil, fmt.Errorf("guestseed: the documents total more than a FAT16 seed holds")
		}
	}
	fatSectors := ((clusters+2)*2 + sectorBytes - 1) / sectorBytes
	rootSectors := rootEntries * 32 / sectorBytes
	total := 1 + 2*fatSectors + rootSectors + clusters*perCluster
	img := make([]byte, total*sectorBytes)

	sum := sha256.New()
	for _, d := range docs {
		fmt.Fprintf(sum, "%s\x00%d\x00", d.name, len(d.body))
		sum.Write(d.body)
	}
	serial := sum.Sum(nil)[:4]

	b := img[:sectorBytes]
	copy(b[0:3], []byte{0xEB, 0x3C, 0x90})
	copy(b[3:11], "SWCSEED ")
	binary.LittleEndian.PutUint16(b[11:], sectorBytes)
	b[13] = byte(perCluster)
	binary.LittleEndian.PutUint16(b[14:], 1)
	b[16] = 2
	binary.LittleEndian.PutUint16(b[17:], rootEntries)
	if total < 0x10000 {
		binary.LittleEndian.PutUint16(b[19:], uint16(total))
	} else {
		binary.LittleEndian.PutUint32(b[32:], uint32(total))
	}
	b[21] = 0xF8
	binary.LittleEndian.PutUint16(b[22:], uint16(fatSectors))
	binary.LittleEndian.PutUint16(b[24:], 32)
	binary.LittleEndian.PutUint16(b[26:], 64)
	b[36] = 0x80
	b[38] = 0x29
	copy(b[39:43], serial)
	copy(b[43:54], padded(Label, 11))
	copy(b[54:62], "FAT16   ")
	b[510], b[511] = 0x55, 0xAA

	fat := make([]byte, fatSectors*sectorBytes)
	binary.LittleEndian.PutUint16(fat[0:], 0xFFF8)
	binary.LittleEndian.PutUint16(fat[2:], 0xFFFF)
	dataStart := (1 + 2*fatSectors + rootSectors) * sectorBytes
	clusterBytes := perCluster * sectorBytes
	next := 2
	for _, d := range docs {
		n := (len(d.body) + clusterBytes - 1) / clusterBytes
		d.first = next
		for i := range n {
			c := next + i
			link := uint16(c + 1)
			if i == n-1 {
				link = 0xFFFF
			}
			binary.LittleEndian.PutUint16(fat[2*c:], link)
		}
		copy(img[dataStart+(next-2)*clusterBytes:], d.body)
		next += n
	}
	for i := range 2 {
		copy(img[(1+i*fatSectors)*sectorBytes:], fat)
	}

	root := img[(1+2*fatSectors)*sectorBytes : dataStart]
	at := 0
	label := root[0:32]
	copy(label[0:11], padded(Label, 11))
	label[11] = 0x08
	binary.LittleEndian.PutUint16(label[24:], fatDate)
	at += 32
	for _, d := range docs {
		for _, e := range longNameEntries(d.name, d.short) {
			copy(root[at:at+32], e)
			at += 32
		}
		e := root[at : at+32]
		copy(e[0:11], d.short)
		e[11] = 0x20
		binary.LittleEndian.PutUint16(e[16:], fatDate)
		binary.LittleEndian.PutUint16(e[18:], fatDate)
		binary.LittleEndian.PutUint16(e[24:], fatDate)
		binary.LittleEndian.PutUint16(e[26:], uint16(d.first))
		binary.LittleEndian.PutUint32(e[28:], uint32(len(d.body)))
		at += 32
	}
	return img, nil
}

func padded(s string, n int) string {
	return s + strings.Repeat(" ", n-len(s))
}

func shortChecksum(short string) byte {
	var sum byte
	for i := range 11 {
		sum = (sum&1)<<7 + sum>>1 + short[i]
	}
	return sum
}

func longNameEntries(name, short string) [][]byte {
	units := utf16.Encode([]rune(name))
	count := (len(units) + 12) / 13
	slots := make([]uint16, count*13)
	for i := range slots {
		switch {
		case i < len(units):
			slots[i] = units[i]
		case i == len(units):
			slots[i] = 0
		default:
			slots[i] = 0xFFFF
		}
	}
	check := shortChecksum(short)
	out := make([][]byte, 0, count)
	for seq := count; seq >= 1; seq-- {
		e := make([]byte, 32)
		e[0] = byte(seq)
		if seq == count {
			e[0] |= 0x40
		}
		e[11] = 0x0F
		e[13] = check
		part := slots[(seq-1)*13 : seq*13]
		k := 0
		for _, r := range [][2]int{{1, 11}, {14, 26}, {28, 32}} {
			for o := r[0]; o < r[1]; o += 2 {
				binary.LittleEndian.PutUint16(e[o:], part[k])
				k++
			}
		}
		out = append(out, e)
	}
	return out
}

func WriteFile(path string, seed Seed) error {
	img, err := Image(seed)
	if err != nil {
		return err
	}
	tmp, err := os.CreateTemp(filepath.Dir(path), ".cidata-")
	if err != nil {
		return fmt.Errorf("guestseed: %w", err)
	}
	if _, err := tmp.Write(img); err != nil {
		tmp.Close()
		os.Remove(tmp.Name())
		return fmt.Errorf("guestseed: writing %s: %w", tmp.Name(), err)
	}
	if err := tmp.Close(); err != nil {
		os.Remove(tmp.Name())
		return fmt.Errorf("guestseed: %w", err)
	}
	if err := os.Chmod(tmp.Name(), 0o444); err != nil {
		os.Remove(tmp.Name())
		return fmt.Errorf("guestseed: %w", err)
	}
	if err := os.Rename(tmp.Name(), path); err != nil {
		os.Remove(tmp.Name())
		return fmt.Errorf("guestseed: placing the seed at %s: %w", path, err)
	}
	return nil
}