parseInt(val, 10) as BlockSize}
- items={[
- { label: '512', value: 512 },
- { label: '2048', value: 2048 },
- { label: '4096', value: 4096 },
- ]}
- />
-
+
+ parseInt(val, 10) as BlockSize}
+ items={[
+ { label: '512', value: 512 },
+ { label: '2048', value: 2048 },
+ { label: '4096', value: 4096 },
+ ]}
+ />
+ {imageValidation && }
+
+
+
+ {imageValidation && }
+
{file && modalOpen && (
@@ -640,3 +655,139 @@ export function CreateImageSideModalForm() {
)
}
+
+function BlockSizeNotice({
+ blockSize,
+ efiPartOffset,
+ isBootableCd,
+}: {
+ blockSize: number
+ efiPartOffset: number
+ isBootableCd: boolean
+}) {
+ const isEfi = efiPartOffset !== -1
+
+ // If the image doesn't look bootable, return null (`BootableNotice` does the work).
+ if (!isEfi && !isBootableCd) return null
+ // If we detect `EFI BOOT` and the block size is set correctly return null.
+ // (This includes hybrid GPT+ISO.)
+ if (isEfi && blockSize === efiPartOffset) return null
+ // If we detect only `CD001` and the block size is set correctly return null.
+ if (!isEfi && isBootableCd && blockSize === 2048) return null
+
+ // Block size is set incorrectly. If we detect `EFI BOOT`, always show that warning.
+ const content = isEfi
+ ? `Detected “EFI PART” marker at offset ${efiPartOffset}, but block size is set to ${blockSize}.`
+ : 'Bootable CDs typically use a block size of 2048.'
+
+ return (
+
+ )
+}
+
+function BootableNotice({
+ efiPartOffset,
+ isBootableCd,
+ isCompressed,
+}: {
+ efiPartOffset: number
+ isBootableCd: boolean
+ isCompressed: boolean
+}) {
+ // this message should only appear if the image doesn't have a header
+ // marker we are looking for and does not appear to be compressed
+ const efiPartOrBootable = efiPartOffset !== -1 || isBootableCd
+ if (efiPartOrBootable && !isCompressed) return null
+
+ const content = (
+
+
+ {!efiPartOrBootable && (
+ -
+
Bootable markers not found at any block size.
+
+ Expected either “EFI PART” marker at offsets 512 / 2048 / 4096 or “CD001” at
+ offset 0x8001 (for a bootable CD).
+
+
+ )}
+ {isCompressed && (
+ -
+
This might be a compressed image.
+
+ Only raw, uncompressed images are supported. Files such as qcow2, vmdk,
+ img.gz, iso.7z may not work.
+
+
+ )}
+
+
+
+ )
+
+ return (
+
+ )
+}
+
+async function readAtOffset(file: File, offset: number, length: number) {
+ const reader = new FileReader()
+
+ const promise = new Promise((resolve, reject) => {
+ reader.onloadend = (e) => {
+ if (
+ e.target?.readyState === FileReader.DONE &&
+ // should always be true because we're using readAsArrayBuffer
+ e.target.result instanceof ArrayBuffer
+ ) {
+ resolve(String.fromCharCode(...new Uint8Array(e.target.result)))
+ return
+ }
+ resolve(undefined)
+ }
+
+ reader.onerror = (error) => {
+ console.error(`Error reading file at offset ${offset}:`, error)
+ reject(error)
+ }
+ })
+
+ reader.readAsArrayBuffer(file.slice(offset, offset + length))
+ return promise
+}
+
+async function getEfiPartOffset(file: File) {
+ const offsets = [512, 2048, 4096]
+ for (const offset of offsets) {
+ const isMatch = (await readAtOffset(file, offset, 8)) === 'EFI PART'
+ if (isMatch) return offset
+ }
+ return -1
+}
+
+const compressedExts = ['.gz', '.7z', '.qcow2', '.vmdk']
+
+const validateImage = async (file: File) => {
+ const lowerFileName = file.name.toLowerCase()
+ return {
+ efiPartOffset: await getEfiPartOffset(file),
+ isBootableCd: (await readAtOffset(file, 0x8001, 5)) === 'CD001',
+ isCompressed: compressedExts.some((ext) => lowerFileName.endsWith(ext)),
+ }
+}
diff --git a/app/util/links.ts b/app/util/links.ts
index e1af292667..9400242fa7 100644
--- a/app/util/links.ts
+++ b/app/util/links.ts
@@ -16,6 +16,8 @@ export const links: Record = {
'https://docs.oxide.computer/guides/configuring-guest-networking#_firewall_rules',
floatingIpsDocs: 'https://docs.oxide.computer/guides/managing-floating-ips',
imagesDocs: 'https://docs.oxide.computer/guides/creating-and-sharing-images',
+ preparingImagesDocs:
+ 'https://docs.oxide.computer/guides/creating-and-sharing-images#_preparing_images_for_import',
instancesDocs: 'https://docs.oxide.computer/guides/managing-instances',
keyConceptsIamPolicyDocs:
'https://docs.oxide.computer/guides/key-entities-and-concepts#iam-policy',