Skip to content

Commit d940b0b

Browse files
committed
metadata
1 parent da490c9 commit d940b0b

4 files changed

Lines changed: 435 additions & 0 deletions

File tree

config/cloudhypervisor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ func (c *Config) CHVMOverlayPath(vmID string) string {
6868
return filepath.Join(c.CHVMRunDir(vmID), "overlay.qcow2")
6969
}
7070

71+
// CHVMCidataPath returns the path for the cloud-init NoCloud cidata disk.
72+
func (c *Config) CHVMCidataPath(vmID string) string {
73+
return filepath.Join(c.CHVMRunDir(vmID), "cidata.img")
74+
}
75+
7176
// FirmwarePath returns the path to the UEFI firmware blob (CLOUDHV.fd).
7277
func (c *Config) FirmwarePath() string {
7378
return filepath.Join(c.RootDir, "firmware", "CLOUDHV.fd")

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Config struct {
2828
// PoolSize is the goroutine pool size for concurrent operations.
2929
// Defaults to runtime.NumCPU() if zero.
3030
PoolSize int `json:"pool_size" mapstructure:"pool_size"`
31+
// DefaultRootPassword is the root password injected into cloudimg VMs
32+
// via cloud-init metadata. Empty means no password is set.
33+
DefaultRootPassword string `json:"default_root_password" mapstructure:"default_root_password"`
3134
// Log configuration, uses eru core's ServerLogConfig.
3235
Log *coretypes.ServerLogConfig `json:"log" mapstructure:"log"`
3336
}

metadata/fat12.go

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
package metadata
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"io"
7+
"sort"
8+
"strings"
9+
"time"
10+
"unicode/utf16"
11+
)
12+
13+
// FAT12 disk layout constants for a 1 MiB image.
14+
const (
15+
sectorSize = 512
16+
totalSectors = 2048 // 1 MiB
17+
sectorsPerClus = 1
18+
reservedSec = 1
19+
numFATs = 2
20+
sectorsPerFAT = 6
21+
rootEntryCount = 128
22+
firstDataSec = reservedSec + numFATs*sectorsPerFAT + rootDirSectors // 21
23+
rootDirSectors = rootEntryCount * dirEntrySize / sectorSize // 8
24+
dirEntrySize = 32
25+
fatEntryEOC = 0xFFF
26+
mediaDesc = 0xF8
27+
)
28+
29+
// CreateFAT12 streams a 1 MiB FAT12 image with VFAT long-filename support to w.
30+
// label is the volume label (e.g. "CIDATA"); files maps filename → content.
31+
func CreateFAT12(w io.Writer, label string, files map[string][]byte) error {
32+
b := newFAT12Builder(label)
33+
34+
// Sort keys for deterministic output.
35+
names := make([]string, 0, len(files))
36+
for name := range files {
37+
names = append(names, name)
38+
}
39+
sort.Strings(names)
40+
41+
for _, name := range names {
42+
if err := b.addFile(name, files[name]); err != nil {
43+
return err
44+
}
45+
}
46+
return b.writeTo(w)
47+
}
48+
49+
// fat12Builder constructs a FAT12 image in memory (FAT + root dir only)
50+
// and streams the full image on writeTo.
51+
type fat12Builder struct {
52+
label string
53+
fat []byte // single FAT copy (written twice)
54+
rootDir []byte // root directory area
55+
data []dataEntry // file data in cluster-allocation order
56+
nextCluster uint16 // next free cluster (starts at 2)
57+
rootUsed int // root directory entries consumed
58+
shortSeq int // counter for ~N short-name suffixes
59+
}
60+
61+
type dataEntry struct {
62+
data []byte
63+
numClusters int
64+
}
65+
66+
func newFAT12Builder(label string) *fat12Builder {
67+
b := &fat12Builder{
68+
label: label,
69+
fat: make([]byte, sectorsPerFAT*sectorSize),
70+
rootDir: make([]byte, rootEntryCount*dirEntrySize),
71+
nextCluster: 2, //nolint:mnd
72+
}
73+
// Reserved FAT entries.
74+
setFATEntry(b.fat, 0, 0xFF8) //nolint:mnd
75+
setFATEntry(b.fat, 1, fatEntryEOC)
76+
b.addVolumeLabel()
77+
return b
78+
}
79+
80+
// addVolumeLabel writes the volume-label directory entry (attribute 0x08).
81+
func (b *fat12Builder) addVolumeLabel() {
82+
name := padLabel(b.label)
83+
off := b.rootUsed * dirEntrySize
84+
copy(b.rootDir[off:], name[:])
85+
b.rootDir[off+11] = 0x08 //nolint:mnd
86+
putTimestamps(b.rootDir[off:], time.Now())
87+
b.rootUsed++
88+
}
89+
90+
// addFile registers a file: allocates FAT clusters, writes LFN + SFN directory entries.
91+
func (b *fat12Builder) addFile(name string, content []byte) error {
92+
numClusters := (len(content) + sectorSize - 1) / sectorSize
93+
94+
var startCluster uint16
95+
if numClusters > 0 {
96+
if int(b.nextCluster)+numClusters > (totalSectors-firstDataSec)+2 { //nolint:mnd
97+
return fmt.Errorf("fat12: not enough space for %s", name)
98+
}
99+
startCluster = b.nextCluster
100+
for i := range numClusters {
101+
c := int(b.nextCluster) + i
102+
if i == numClusters-1 {
103+
setFATEntry(b.fat, c, fatEntryEOC)
104+
} else {
105+
setFATEntry(b.fat, c, uint16(c+1)) //nolint:gosec
106+
}
107+
}
108+
b.data = append(b.data, dataEntry{data: content, numClusters: numClusters})
109+
b.nextCluster += uint16(numClusters)
110+
}
111+
112+
// Generate 8.3 short name — with ~N suffix when LFN is needed.
113+
lfn := needsLFN(name)
114+
var shortName [11]byte
115+
if lfn {
116+
b.shortSeq++
117+
shortName = generateShortName(name, b.shortSeq)
118+
lfnEntries := makeLFNEntries(name, shortName)
119+
for _, entry := range lfnEntries {
120+
if b.rootUsed >= rootEntryCount {
121+
return fmt.Errorf("fat12: root directory full")
122+
}
123+
off := b.rootUsed * dirEntrySize
124+
copy(b.rootDir[off:], entry)
125+
b.rootUsed++
126+
}
127+
} else {
128+
shortName = toShortName(name)
129+
}
130+
131+
if b.rootUsed >= rootEntryCount {
132+
return fmt.Errorf("fat12: root directory full")
133+
}
134+
off := b.rootUsed * dirEntrySize
135+
copy(b.rootDir[off:], shortName[:])
136+
b.rootDir[off+11] = 0x20 // archive //nolint:mnd
137+
putTimestamps(b.rootDir[off:], time.Now())
138+
binary.LittleEndian.PutUint16(b.rootDir[off+26:], startCluster) //nolint:mnd
139+
binary.LittleEndian.PutUint32(b.rootDir[off+28:], uint32(len(content))) //nolint:mnd,gosec
140+
b.rootUsed++
141+
return nil
142+
}
143+
144+
// writeTo streams: boot sector → FAT ×2 → root directory → data → zero padding.
145+
func (b *fat12Builder) writeTo(w io.Writer) error {
146+
if _, err := w.Write(b.makeBootSector()); err != nil {
147+
return err
148+
}
149+
for range numFATs {
150+
if _, err := w.Write(b.fat); err != nil {
151+
return err
152+
}
153+
}
154+
if _, err := w.Write(b.rootDir); err != nil {
155+
return err
156+
}
157+
158+
sector := make([]byte, sectorSize)
159+
dataSectors := 0
160+
for _, e := range b.data {
161+
for i := range e.numClusters {
162+
clear(sector)
163+
start := i * sectorSize
164+
if start < len(e.data) {
165+
copy(sector, e.data[start:min(start+sectorSize, len(e.data))])
166+
}
167+
if _, err := w.Write(sector); err != nil {
168+
return err
169+
}
170+
dataSectors++
171+
}
172+
}
173+
174+
// Zero-fill the remaining data area.
175+
clear(sector)
176+
for range totalSectors - firstDataSec - dataSectors {
177+
if _, err := w.Write(sector); err != nil {
178+
return err
179+
}
180+
}
181+
return nil
182+
}
183+
184+
func (b *fat12Builder) makeBootSector() []byte {
185+
boot := make([]byte, sectorSize)
186+
187+
// x86 jump + NOP
188+
boot[0], boot[1], boot[2] = 0xEB, 0x3C, 0x90 //nolint:mnd
189+
190+
copy(boot[3:], "COCOON ") //nolint:mnd
191+
binary.LittleEndian.PutUint16(boot[11:], sectorSize) //nolint:mnd
192+
boot[13] = sectorsPerClus //nolint:mnd
193+
binary.LittleEndian.PutUint16(boot[14:], reservedSec) //nolint:mnd
194+
boot[16] = numFATs //nolint:mnd
195+
binary.LittleEndian.PutUint16(boot[17:], rootEntryCount) //nolint:mnd
196+
binary.LittleEndian.PutUint16(boot[19:], totalSectors) //nolint:mnd
197+
boot[21] = mediaDesc //nolint:mnd
198+
binary.LittleEndian.PutUint16(boot[22:], sectorsPerFAT) //nolint:mnd
199+
binary.LittleEndian.PutUint16(boot[24:], 32) // sectors per track //nolint:mnd
200+
binary.LittleEndian.PutUint16(boot[26:], 64) // heads //nolint:mnd
201+
boot[36] = 0x80 // drive number //nolint:mnd
202+
boot[38] = 0x29 // extended boot signature //nolint:mnd
203+
binary.LittleEndian.PutUint32(boot[39:], uint32(time.Now().UnixNano())) //nolint:mnd,gosec
204+
205+
label := padLabel(b.label)
206+
copy(boot[43:54], label[:]) //nolint:mnd
207+
copy(boot[54:62], "FAT12 ") //nolint:mnd
208+
boot[510], boot[511] = 0x55, 0xAA //nolint:mnd
209+
return boot
210+
}
211+
212+
// --- FAT12 entry encoding ---
213+
214+
// setFATEntry writes a 12-bit value into the FAT at the given cluster index.
215+
func setFATEntry(fat []byte, cluster int, val uint16) {
216+
off := cluster + cluster/2 //nolint:mnd
217+
word := uint16(fat[off]) | uint16(fat[off+1])<<8
218+
if cluster%2 == 0 { //nolint:mnd
219+
word = (word & 0xF000) | (val & 0x0FFF) //nolint:mnd
220+
} else {
221+
word = (word & 0x000F) | ((val & 0x0FFF) << 4) //nolint:mnd
222+
}
223+
fat[off] = byte(word)
224+
fat[off+1] = byte(word >> 8) //nolint:mnd
225+
}
226+
227+
// --- directory helpers ---
228+
229+
// needsLFN reports whether name requires VFAT long-filename entries.
230+
func needsLFN(name string) bool {
231+
upper := strings.ToUpper(name)
232+
var base, ext string
233+
if dot := strings.LastIndex(upper, "."); dot >= 0 {
234+
base = upper[:dot]
235+
ext = upper[dot+1:]
236+
} else {
237+
base = upper
238+
}
239+
return len(base) > 8 || len(ext) > 3 || name != upper || strings.Count(name, ".") > 1 //nolint:mnd
240+
}
241+
242+
// toShortName pads a simple name (already fits 8.3, uppercase) into an 11-byte SFN.
243+
func toShortName(name string) [11]byte {
244+
var result [11]byte
245+
for i := range result {
246+
result[i] = ' '
247+
}
248+
upper := strings.ToUpper(name)
249+
if dot := strings.LastIndex(upper, "."); dot >= 0 {
250+
copy(result[:8], upper[:dot]) //nolint:mnd
251+
copy(result[8:], upper[dot+1:]) //nolint:mnd
252+
} else {
253+
copy(result[:8], upper) //nolint:mnd
254+
}
255+
return result
256+
}
257+
258+
// generateShortName builds an 8.3 name with numeric tail (e.g. "META-D~1 ").
259+
func generateShortName(name string, seq int) [11]byte {
260+
var result [11]byte
261+
for i := range result {
262+
result[i] = ' '
263+
}
264+
265+
upper := strings.ToUpper(name)
266+
var base, ext string
267+
if dot := strings.LastIndex(upper, "."); dot >= 0 {
268+
base = strings.ReplaceAll(upper[:dot], ".", "")
269+
ext = upper[dot+1:]
270+
} else {
271+
base = upper
272+
}
273+
274+
tail := fmt.Sprintf("~%d", seq)
275+
maxBase := 8 - len(tail) //nolint:mnd
276+
if len(base) > maxBase {
277+
base = base[:maxBase]
278+
}
279+
copy(result[:8], base+tail) //nolint:mnd
280+
if len(ext) > 3 { //nolint:mnd
281+
ext = ext[:3] //nolint:mnd
282+
}
283+
copy(result[8:], ext) //nolint:mnd
284+
return result
285+
}
286+
287+
// --- VFAT LFN ---
288+
289+
// makeLFNEntries creates VFAT long-filename directory entries in disk order
290+
// (highest sequence number first, immediately before the SFN entry).
291+
func makeLFNEntries(name string, shortName [11]byte) [][]byte {
292+
runes := utf16.Encode([]rune(name))
293+
chksum := lfnChecksum(shortName)
294+
numEntries := (len(runes) + 12) / 13 //nolint:mnd
295+
296+
entries := make([][]byte, numEntries)
297+
for i := range numEntries {
298+
entry := make([]byte, dirEntrySize)
299+
300+
seq := byte(i + 1)
301+
if i == numEntries-1 {
302+
seq |= 0x40 //nolint:mnd
303+
}
304+
entry[0] = seq
305+
entry[11] = 0x0F // LFN attribute //nolint:mnd
306+
entry[13] = chksum //nolint:mnd
307+
308+
base := i * 13 //nolint:mnd
309+
putLFNChars(entry[1:11], runes, base, 5) //nolint:mnd
310+
putLFNChars(entry[14:26], runes, base+5, 6) //nolint:mnd
311+
putLFNChars(entry[28:32], runes, base+11, 2) //nolint:mnd
312+
313+
entries[i] = entry
314+
}
315+
316+
// Reverse: highest sequence first on disk.
317+
for i, j := 0, len(entries)-1; i < j; i, j = i+1, j-1 {
318+
entries[i], entries[j] = entries[j], entries[i]
319+
}
320+
return entries
321+
}
322+
323+
// putLFNChars fills UCS-2 character slots: data char, then null terminator, then 0xFFFF padding.
324+
func putLFNChars(dst []byte, runes []uint16, offset, count int) {
325+
for j := range count {
326+
idx := offset + j
327+
pos := j * 2 //nolint:mnd
328+
switch {
329+
case idx < len(runes):
330+
binary.LittleEndian.PutUint16(dst[pos:], runes[idx])
331+
case idx == len(runes):
332+
// null terminator
333+
default:
334+
binary.LittleEndian.PutUint16(dst[pos:], 0xFFFF) //nolint:mnd
335+
}
336+
}
337+
}
338+
339+
func lfnChecksum(shortName [11]byte) byte {
340+
var sum byte
341+
for _, b := range shortName {
342+
sum = ((sum >> 1) | (sum << 7)) + b //nolint:mnd
343+
}
344+
return sum
345+
}
346+
347+
// --- timestamp helpers ---
348+
349+
func padLabel(label string) [11]byte {
350+
var result [11]byte
351+
for i := range result {
352+
result[i] = ' '
353+
}
354+
copy(result[:], strings.ToUpper(label))
355+
return result
356+
}
357+
358+
func putTimestamps(entry []byte, t time.Time) {
359+
date, tm := encodeFATDateTime(t)
360+
binary.LittleEndian.PutUint16(entry[14:], tm) //nolint:mnd
361+
binary.LittleEndian.PutUint16(entry[16:], date) //nolint:mnd
362+
binary.LittleEndian.PutUint16(entry[18:], date) // last access //nolint:mnd
363+
binary.LittleEndian.PutUint16(entry[22:], tm) //nolint:mnd
364+
binary.LittleEndian.PutUint16(entry[24:], date) //nolint:mnd
365+
}
366+
367+
func encodeFATDateTime(t time.Time) (date, fatTime uint16) {
368+
date = uint16((t.Year()-1980)<<9) | uint16(int(t.Month())<<5) | uint16(t.Day()) //nolint:mnd,gosec
369+
fatTime = uint16(t.Hour()<<11) | uint16(t.Minute()<<5) | uint16(t.Second()/2) //nolint:mnd,gosec
370+
return
371+
}

0 commit comments

Comments
 (0)