/00000000", "Ref:/00000000.checksum"... Here: what XenCenter needs to read the VM's * name before an import (readHeader(), the first 512-byte header and ova.xml only, never the whole file), and a small writer the * demo pool uses to answer /export. */ final class Xva { /** One tar member: a 512-byte ustar header, the data, padding to 512. */ public static function member(string $name, string $data, int $mtime = 0): string { $header = str_pad($name, 100, "\0") . str_pad('0000644', 7, '0', STR_PAD_LEFT) . "\0" . "0000000\0" . "0000000\0" . str_pad(decoct(strlen($data)), 11, '0', STR_PAD_LEFT) . "\0" . str_pad(decoct($mtime), 11, '0', STR_PAD_LEFT) . "\0" . ' ' . '0' . str_repeat("\0", 100) . "ustar\0" . '00' . str_repeat("\0", 32) . str_repeat("\0", 32) . str_repeat("\0", 8) . str_repeat("\0", 8) . str_repeat("\0", 155); $header = str_pad($header, 512, "\0"); $sum = 0; for ($i = 0; $i < 512; $i++) { $sum += ord($header[$i]); } $header = substr_replace($header, str_pad(decoct($sum), 6, '0', STR_PAD_LEFT) . "\0 ", 148, 8); $pad = (512 - strlen($data) % 512) % 512; return $header . $data . str_repeat("\0", $pad); } /** The end of a tar archive: two empty blocks. */ public static function end(): string { return str_repeat("\0", 1024); } /** * The ova.xml of a VM: the "objects" list XAPI writes (class, id, snapshot = the record), the fields an import needs. * * @param array $vm * @param list $disks */ public static function ovaXml(array $vm, array $disks): string { $member = fn (string $name, string $value): string => '' . $name . '' . htmlspecialchars($value, ENT_XML1) . ''; $objects = '' . $member('class', 'VM') . $member('id', 'Ref:0') . 'snapshot' . $member('name_label', (string)($vm['name_label'] ?? 'VM')) . $member('name_description', (string)($vm['name_description'] ?? '')) . $member('memory_static_max', sprintf('%.0F', (float)($vm['memory_static_max'] ?? 0))) . $member('VCPUs_max', (string)(int)($vm['VCPUs_max'] ?? 1)) . $member('power_state', 'Halted') . $member('is_a_template', 'false') . ''; foreach ($disks as $disk) { $objects .= '' . $member('class', 'VDI') . $member('id', $disk['id']) . 'snapshot' . $member('name_label', $disk['name']) . $member('virtual_size', sprintf('%.0F', $disk['size'])) . ''; } return 'version' . $member('hostname', 'xcp-ng') . $member('product_version', '8.3.0') . $member('xapi_major', '24') . $member('export_vsn', '2') . 'objects' . $objects . ''; } /** * What ova.xml says of the VM (name_label, name_description, memory_static_max, VCPUs_max, disks: [[name, virtual_size]]), or null * when it is not an XVA. $head: the first bytes of the file (ova.xml is the first member: a few kilobytes). * * @return array|null */ public static function readHeader(string $head): ?array { if (str_starts_with($head, "\x1f\x8b") && function_exists('inflate_init')) { // an export made with use_compression: the start of the gzip stream is enough for ova.xml $inflate = inflate_init(ZLIB_ENCODING_GZIP); $head = $inflate === false ? '' : (string)@inflate_add($inflate, $head, ZLIB_SYNC_FLUSH); } if (strlen($head) < 512 || rtrim(substr($head, 0, 100), "\0") !== 'ova.xml') { return null; } $size = (int)octdec(trim(substr($head, 124, 12), "\0 ")); $xml = substr($head, 512, $size); $objects = preg_split('#class#', $xml) ?: []; $out = null; $disks = []; foreach (array_slice($objects, 1) as $object) { $class = preg_match('#^([^<]*)#', $object, $m) ? $m[1] : ''; $field = fn (string $name): ?string => preg_match('#' . $name . '(?:)?([^<]*)#', $object, $f) ? html_entity_decode($f[1], ENT_XML1) : null; if ($class === 'VM' && $out === null) { $out = ['name_label' => (string)$field('name_label'), 'name_description' => (string)$field('name_description'), 'memory_static_max' => (float)$field('memory_static_max'), 'VCPUs_max' => (int)$field('VCPUs_max')]; } elseif ($class === 'VDI') { $disks[] = [(string)$field('name_label'), (float)$field('virtual_size')]; } } if ($out === null) { return null; } return $out + ['disks' => $disks]; } /** The first bytes of a file, enough for readHeader() (ova.xml stays small). */ public static function headOf(string $file, int $bytes = 262144): string { $handle = @fopen($file, 'rb'); if ($handle === false) { return ''; } $head = (string)fread($handle, $bytes); fclose($handle); return $head; } }