├── .flake8 ├── .gitignore ├── Makefile ├── kvmboot ├── README.md ├── grub.cfg.example ├── mkgrubcfg.py └── parseiso.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | extend-ignore = 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | fonts/ 2 | i386-pc/ 3 | locale/ 4 | x86_64-efi/ 5 | grubenv 6 | *.mod 7 | *.img 8 | *.o 9 | *.lst 10 | grub.cfg 11 | __pycache__ 12 | .mypy_cache/ 13 | grub.cfg.old 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | 4 | grub.cfg: mkgrubcfg.py ../../ubuntu ../../ubuntu/*.iso 5 | python3 mkgrubcfg.py -o grub.cfg 6 | 7 | diff: 8 | diff -u grub.cfg <(python3 mkgrubcfg.py) 9 | -------------------------------------------------------------------------------- /kvmboot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEVICE=${1:-/dev/sda1} 4 | ENTIRE_DEVICE=${DEVICE%%[0-9]} 5 | USER=$(id -u) 6 | 7 | set -x 8 | udisksctl unmount -b "$DEVICE" || exit 1 9 | sudo setfacl -m user:"$USER":r "$ENTIRE_DEVICE" 10 | kvm -m 2048 -k en-us -drive format=raw,if=virtio,readonly,file="$ENTIRE_DEVICE" 11 | udisksctl mount -b "$DEVICE" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bootable USB disk that lets you choose an ISO image 2 | =================================================== 3 | 4 | This is basically a newer iteration on 5 | https://mg.pov.lt/blog/booting-iso-from-usb.html 6 | 7 | Creating a bootable USB disk that lets you boot any Ubuntu ISO image: 8 | 9 | 1. Mount a USB disk with a sufficient amount of free space. Note the device 10 | name (e.g. `/dev/sdb`) and the mount point (e.g. `/media/mg/MG-FLASH`). 11 | 12 | 2. Install GRUB: 13 | 14 | ``` 15 | sudo grub-install --target=i386-pc \ 16 | --root-directory=/media/mg/MG-FLASH /dev/sdb 17 | ``` 18 | 19 | (you may have to also use `--force`) 20 | 21 | 3. Perhaps also install an UEFI bootloader 22 | 23 | ``` 24 | sudo grub-install --target=x86_64-efi --removable \ 25 | --root-directory=/media/mg/MG-FLASH \ 26 | --efi-directory=/media/mg/MG-FLASH /dev/sdb 27 | ``` 28 | 29 | (be very careful not to forget `--removable`, or it'll overwrite your EFI 30 | boot variables and your host machine will fail to boot!) 31 | 32 | 4. Download Ubuntu ISO images you want 33 | 34 | ``` 35 | cd /media/mg/MG-FLASH 36 | git clone https://github.com/mgedmin/ubuntu-images ubuntu 37 | cd ubuntu 38 | # maybe edit the Makefile to pick what Ubuntu versions and variants you want 39 | make verify-all 40 | ``` 41 | 42 | 5. Check out this repository (this is tricky because git doesn't want to check 43 | out things into an existing non-empty directory) 44 | 45 | ``` 46 | git clone https://github.com/mgedmin/bootable-iso /tmp/bootable-iso 47 | mv /tmp/bootable-iso/.git /media/mg/MG-FLASH/boot/grub/ 48 | mv /tmp/bootable-iso/* /media/mg/MG-FLASH/boot/grub/ 49 | ``` 50 | 51 | 6. Run `make -C /media/mg/MG-FLASH/boot/grub/` to build a `grub.cfg` that 52 | matches your Ubuntu images 53 | 54 | 7. Test that things work 55 | 56 | 57 | Testing with KVM 58 | ---------------- 59 | 60 | 1. Find the device name 61 | 62 | ``` 63 | udisksctl status 64 | ``` 65 | 66 | 2. Run 67 | 68 | ``` 69 | sh kvmboot /dev/sdb1 70 | ``` 71 | 72 | 3. Record the test results in mkgrubcfg.py 73 | 74 | 75 | Other resources 76 | --------------- 77 | 78 | - https://help.ubuntu.com/community/Grub2/ISOBoot 79 | 80 | - You may be interested in [Ventoy](https://www.ventoy.net/), which seems a 81 | much more polished and capable solution for this kind of thing. 82 | -------------------------------------------------------------------------------- /grub.cfg.example: -------------------------------------------------------------------------------- 1 | # 2 | # Notes for adding new entries: 3 | # - launch mc, find the iso, press Enter 4 | # - inside the ISO find /boot/grub/grub.cfg, look inside 5 | # be sure to add iso-scan/filename=$isofile before the -- or --- 6 | # (some ISOs have a loopback.cfg that already have this) 7 | # 8 | # Testing in KVM: 9 | # - udisksctl unmount -b /dev/sdb1 10 | # - sudo setfacl -m user:$USER:rw /dev/sdb 11 | # - kvm -m 2048 -k en-us -drive format=raw,file=/dev/sdb 12 | # (if arrow keys don't work in the GRUB menu, use Ctrl-N/P) 13 | # - udisksctl mount -b /dev/sdb1 14 | # 15 | 16 | 17 | submenu "Ubuntu 20.04 LTS >" { 18 | 19 | menuentry "Ubuntu 20.04 LTS (x86-64 desktop livecd)" { 20 | # Untested 21 | set isofile="/ubuntu/ubuntu-20.04-desktop-amd64.iso" 22 | loopback loop $isofile 23 | # NB: add only-ubiquity to kernel command line prior to --- to launch just the installer 24 | linux (loop)/casper/vmlinuz iso-scan/filename=$isofile file=/cdrom/preseed/ubuntu.seed maybe-ubiquity quiet splash --- 25 | initrd (loop)/casper/initrd 26 | } 27 | 28 | menuentry "Ubuntu 20.04 LTS (x86-64 server livecd)" { 29 | # Untested 30 | set isofile="/ubuntu/ubuntu-20.04-live-server-amd64.iso" 31 | loopback loop $isofile 32 | linux (loop)/casper/vmlinuz iso-scan/filename=$isofile quiet --- 33 | initrd (loop)/casper/initrd 34 | } 35 | 36 | } # end of submenu 37 | 38 | menuentry "Ubuntu 18.04.4 LTS (x86-64 server livecd)" { 39 | # Untested 40 | set isofile="/ubuntu/ubuntu-18.04.4-live-server-amd64.iso" 41 | loopback loop $isofile 42 | linux (loop)/casper/vmlinuz iso-scan/filename=$isofile boot=casper quiet --- 43 | initrd (loop)/casper/initrd 44 | } 45 | 46 | menuentry "Ubuntu 16.04.6 LTS (x86-64 server)" { 47 | # Untested 48 | set isofile="/ubuntu/ubuntu-16.04.6-server-amd64.iso" 49 | loopback loop $isofile 50 | linux (loop)/casper/vmlinuz iso-scan/filename=$isofile file=/cdrom/preseed/ubuntu-server.seed boot=casper quiet --- 51 | initrd (loop)/casper/initrd 52 | } 53 | 54 | 55 | ##submenu "Firmware upgrade images >" { 56 | ## 57 | ##menuentry "Lenovo ThinkPad X200 BIOS update bootable CD (version 3.21)" { 58 | ## # Works! 59 | ## # See also: /boot/x200-bios/*.iso 60 | ## # See also: http://www.donarmstrong.com/posts/x200_bios_update/ 61 | ## set memdisk="/boot/syslinux-memdisk" 62 | ## set imgfile="/boot/lenovo-thinkpad-x200-bios.img" 63 | ## linux16 $memdisk 64 | ## initrd16 $imgfile 65 | ##} 66 | ## 67 | ##menuentry "Lenovo ThinkPad X200 BIOS update bootable CD (version 3.21) - alternative boot method" { 68 | ## # Not tested 69 | ## set memdisk="/boot/syslinux-memdisk" 70 | ## set isofile="/boot/x200-bios/6duj46uc.iso" 71 | ## linux16 $memdisk iso 72 | ## initrd16 $isofile 73 | ##} 74 | ## 75 | ##menuentry "Intel SSD firmware update (version 1.92)" { 76 | ## set memdisk="/boot/syslinux-memdisk" 77 | ## set imgfile="/boot/intel-ssd-firmware.img" 78 | ## linux16 $memdisk 79 | ## initrd16 $imgfile 80 | ##} 81 | ## 82 | ##} # end of submenu 83 | 84 | menuentry "Memory test (memtest86+)" { 85 | linux16 /boot/mt86plus 86 | } 87 | -------------------------------------------------------------------------------- /mkgrubcfg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Generate a grub.cfg that can boot Ubuntu ISO images. 4 | 5 | Works with any Ubuntu ISO image that uses casper 6 | (http://manpages.ubuntu.com/manpages/focal/man7/casper.7.html), which is every 7 | desktop and live-server image. 8 | 9 | Usage: mkgrubcfg.py -d path/to/directory/with/iso/images -o grub.cfg 10 | """ 11 | 12 | import argparse 13 | import os 14 | import re 15 | import sys 16 | 17 | 18 | HEADER = """ 19 | # 20 | # Notes for adding new entries: 21 | # - run python3 mkgrubcfg.py -o grub.cfg 22 | # 23 | # Testing in KVM (assuming this USB drive is mounted as /dev/sdb1): 24 | # - run sh kvmboot /dev/sdb1 25 | # (if arrow keys don't work in the GRUB menu, use Ctrl-N/P) 26 | # 27 | 28 | """.lstrip() 29 | 30 | ARCHS = { 31 | 'i386': 'x86', 32 | 'amd64': 'x86-64', 33 | } 34 | 35 | VARIANTS = { 36 | 'desktop': 'desktop livecd', 37 | 'live-server': 'server livecd', 38 | } 39 | 40 | KNOWN_COMMAND_LINES = { 41 | # if you wish to override a command line, or if the autodetection doesn't work, 42 | # you can do it like this: 43 | 'ubuntu-16.04.6-server-amd64.iso': 'file=/cdrom/preseed/ubuntu-server.seed quiet ---', 44 | # NB: this is pointless for ubuntu 16.04 LTS images, they're known not to work: 45 | # they don't use casper, and debian-instaler doesn't know about iso-scan/filename= 46 | # and so the installation fails a couple of steps in when it fails to find the .deb files 47 | } 48 | 49 | KVM_OK = "Tested in KVM, works" 50 | KVM_DESKTOP_OK = "Tested in KVM, works (boots into live session)" 51 | KVM_SERVER_OK = "Tested in KVM, works (boots, haven't tried to complete installation)" 52 | TEST_STATUS = { 53 | # images I have tested personally 54 | 'ubuntu-20.04-desktop-amd64.iso': KVM_OK, 55 | 'ubuntu-20.04-live-server-amd64.iso': KVM_OK, 56 | 'ubuntu-20.04.1-desktop-amd64.iso': KVM_DESKTOP_OK, 57 | 'ubuntu-20.04.1-live-server-amd64.iso': KVM_SERVER_OK, 58 | 'ubuntu-19.10-desktop-amd64.iso': KVM_DESKTOP_OK, 59 | 'ubuntu-18.04.3-desktop-amd64.iso': KVM_DESKTOP_OK, 60 | 'ubuntu-18.04.4-desktop-amd64.iso': KVM_DESKTOP_OK, 61 | 'ubuntu-18.04.3-live-server-amd64.iso': KVM_OK, 62 | 'ubuntu-18.04.4-live-server-amd64.iso': KVM_OK, 63 | 'ubuntu-18.04.5-live-server-amd64.iso': KVM_SERVER_OK, 64 | 'ubuntu-16.04.6-desktop-i386.iso': KVM_DESKTOP_OK, 65 | # and this is why overriding the command line can be futile, when autodetection doesn't work: 66 | 'ubuntu-16.04.6-server-amd64.iso': 'Does not work', 67 | } 68 | 69 | ENTRY = """ 70 | menuentry "{title}" {{ 71 | # {test_status} 72 | set isofile="/ubuntu/{isofile}" 73 | loopback loop $isofile 74 | linux (loop){kernel} iso-scan/filename=$isofile {cmdline} 75 | initrd (loop){initrd} 76 | }} 77 | 78 | """.lstrip() 79 | 80 | SUBMENU = """ 81 | submenu "{title} >" {{ 82 | 83 | {entries} 84 | 85 | }} # end of submenu 86 | 87 | """.lstrip() 88 | 89 | FOOTER = """ 90 | menuentry "Memory test (memtest86+)" { 91 | linux16 /boot/mt86plus 92 | } 93 | """.lstrip() 94 | 95 | 96 | class Error(Exception): 97 | pass 98 | 99 | 100 | def find_iso_files(where): 101 | # We want to sort by Ubuntu version, in descending order, and then by image 102 | # type, in ascending alphabetical order. 103 | return sorted( 104 | sorted(fn for fn in os.listdir(where) if fn.endswith('.iso')), 105 | key=lambda fn: fn.split('-')[:2], 106 | reverse=True) 107 | 108 | 109 | def group_files(files): 110 | groups = [] 111 | current = [] 112 | current_prefix = None 113 | for file in files: 114 | prefix = file[:len('ubuntu-XX.YY')] 115 | if prefix == current_prefix: 116 | current.append(file) 117 | else: 118 | if len(current) == 1: 119 | groups.extend(current) 120 | elif current: 121 | groups.append(current) 122 | current_prefix = prefix 123 | current = [file] 124 | if len(current) == 1: 125 | groups.extend(current) 126 | elif current: 127 | groups.append(current) 128 | return groups 129 | 130 | 131 | def make_grub_cfg(entries, isodir): 132 | parts = [HEADER] 133 | for entry_or_group in entries: 134 | if isinstance(entry_or_group, list): 135 | title = mkgrouptitle(entry_or_group) 136 | parts.append(mksubmenu(title, [ 137 | mkentry(entry, isodir) for entry in entry_or_group 138 | ])) 139 | else: 140 | parts.append(mkentry(entry_or_group, isodir)) 141 | parts.append(FOOTER) 142 | return ''.join(parts) 143 | 144 | 145 | def mkgrouptitle(isofiles): 146 | # ubuntu-XX.XX-variant-arch.iso 147 | ubuntu, release, rest = isofiles[0].rpartition('.')[0].split('-', 2) 148 | if is_lts(release): 149 | release += ' LTS' 150 | return f'Ubuntu {release}' 151 | 152 | 153 | def mksubmenu(title, entries): 154 | return SUBMENU.format( 155 | title=title, 156 | entries=''.join(entries).rstrip(), 157 | ) 158 | 159 | 160 | def mkentry(isofile, isodir): 161 | try: 162 | return ENTRY.format( 163 | isofile=isofile, 164 | title=mktitle(isofile), 165 | test_status=mkteststatus(isofile), 166 | cmdline=mkcmdline(isofile, isodir), 167 | kernel='/casper/vmlinuz', 168 | initrd='/casper/initrd', # some older releases had initrd.gz 169 | ).replace('\n # \n', '\n') # remove empty comments 170 | except Error as err: 171 | print(f"skipping {isofile}: {err}", file=sys.stderr) 172 | return '' 173 | 174 | 175 | def mktitle(isofile): 176 | # ubuntu-XX.XX-variant-arch.iso 177 | try: 178 | ubuntu, release, rest = isofile.rpartition('.')[0].split('-', 2) 179 | if ubuntu != 'ubuntu': 180 | raise ValueError 181 | variant, arch = rest.rsplit('-', 1) 182 | if is_lts(release): 183 | release += ' LTS' 184 | except ValueError: 185 | raise Error(f'filename does not look like ubuntu-XX.XX-variant-arch.iso') 186 | return f'Ubuntu {release} ({ARCHS.get(arch, arch)} {VARIANTS.get(variant, variant)})' 187 | 188 | 189 | def mkteststatus(isofile): 190 | return '\n # '.join(get_test_status(isofile)) 191 | 192 | 193 | def get_test_status(isofile): 194 | test_status = TEST_STATUS.get(isofile, 'Untested') 195 | if not isinstance(test_status, list): 196 | test_status = [test_status] 197 | return test_status 198 | 199 | 200 | def mkcmdline(isofile, isodir): 201 | # too risky to guess 202 | if isofile in KNOWN_COMMAND_LINES: 203 | cmdline = KNOWN_COMMAND_LINES[isofile] 204 | else: 205 | cmdline = extract_command_line_from_iso(os.path.join(isodir, isofile)) 206 | 207 | if isinstance(cmdline, dict): 208 | cmdline = cmdline[None] 209 | return cmdline 210 | 211 | 212 | def extract_command_line_from_iso(isofile): 213 | from parseiso import parse_iso, FormatError 214 | try: 215 | with parse_iso(isofile) as walker: 216 | grub_cfg = walker.read('/boot/grub/grub.cfg').decode('UTF-8', 'replace') 217 | except (OSError, FormatError) as e: 218 | raise Error(str(e)) 219 | return extract_command_line_from_grub_cfg(grub_cfg) 220 | 221 | 222 | def extract_command_line_from_grub_cfg(grub_cfg_text): 223 | rejected = [] 224 | for menuentry, linux, kernel, cmdline in extract_grub_menu(grub_cfg_text): 225 | if (linux, kernel) == ('linux', '/casper/vmlinuz'): 226 | return cmdline 227 | rejected.append((menuentry, f"{linux} {kernel} {cmdline}")) 228 | error = 'could not find a suitable kernel command line in grub.cfg inside the ISO image' 229 | if rejected: 230 | error += '\nrejected, because they use the wrong kernel (not /casper/vmlinuz):\n' 231 | for menuentry, line in rejected: 232 | error += f' menuentry "{menuentry}"\n {line}\n' 233 | error += 'if you want to use one of these, edit mkgrubcfg.py and modify KNOWN_COMMAND_LINES' 234 | raise Error(error) 235 | 236 | 237 | def extract_grub_menu(grub_cfg_text): 238 | menuentry_rx = re.compile(r'^\s*menuentry\s+"([^"]+)"') 239 | linux_rx = re.compile(r'^\s*(linux|linux16)\s+(\S+)\s+(\S.*)') 240 | menuentry = None 241 | for line in grub_cfg_text.splitlines(): 242 | m = menuentry_rx.match(line) 243 | if m: 244 | menuentry = m.group(1) 245 | m = linux_rx.match(line) 246 | if m: 247 | linux, kernel, cmdline = m.groups() 248 | yield (menuentry, linux, kernel, cmdline) 249 | 250 | 251 | def is_lts(release): 252 | major, minor = map(int, release.split('.')[:2]) 253 | # 6.06 was the first LTS release; since then there's been an LTS every two 254 | # years: 8.04, 10.04, 12.04, 14.04, 16.04, 18.04 and 20.04. 255 | # we can ignore the past and focus on the current pattern. 256 | return major % 2 == 0 and minor == 4 257 | 258 | 259 | def print_groups(groups): 260 | for group in groups: 261 | if not isinstance(group, list): 262 | group = [group] 263 | print(f'# {mkgrouptitle(group)}') 264 | for isofile in group: 265 | width = 40 266 | test_status = f'{" ":<{width}} # '.join(get_test_status(isofile)) 267 | print(f'{isofile:<{width}} # {test_status}') 268 | 269 | 270 | def main(): 271 | parser = argparse.ArgumentParser(description="create a grub.cfg for Ubuntu ISO images") 272 | parser.add_argument("--list", action='store_true', help='list found ISO images') 273 | parser.add_argument("-d", "--iso-dir", metavar='DIR', default="../../ubuntu", 274 | help="directory with ISO images (default: %(default)s)") 275 | parser.add_argument("-o", metavar='FILENAME', dest='outfile', default="-", 276 | help="write the generated grub.cfg to this file (default: stdout)") 277 | args = parser.parse_args() 278 | 279 | try: 280 | iso_files = find_iso_files(args.iso_dir) 281 | groups = group_files(iso_files) 282 | if args.list: 283 | print_groups(groups) 284 | return 285 | grub_cfg = make_grub_cfg(groups, args.iso_dir) 286 | if args.outfile != '-': 287 | with open(args.outfile, 'w') as f: 288 | f.write(grub_cfg) 289 | else: 290 | print(grub_cfg, end="") 291 | except Error as e: 292 | sys.exit(str(e)) 293 | 294 | 295 | if __name__ == "__main__": 296 | main() 297 | -------------------------------------------------------------------------------- /parseiso.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Extract a file (usually /boot/grub/grub.cfg) from an ISO image or list 4 | directory contents. 5 | 6 | Examples: 7 | 8 | python3 parseiso.py filename.iso --ls / 9 | python3 parseiso.py filename.iso --ls /boot/grub 10 | python3 parseiso.py filename.iso --path /boot/grub/grub.cfg 11 | python3 parseiso.py filename.iso --path /md5sum.txt 12 | python3 parseiso.py --help 13 | 14 | """ 15 | 16 | import argparse 17 | import enum 18 | import pathlib 19 | import struct 20 | import sys 21 | import typing 22 | from contextlib import contextmanager 23 | 24 | 25 | class FormatError(Exception): 26 | pass 27 | 28 | 29 | @contextmanager 30 | def parse_iso(isofile): 31 | with open(isofile, 'rb') as f: 32 | # ISO9660: the first 32 KB are unused and can be used for other 33 | # purposes (e.g. hybrid CDs) 34 | f.seek(32768) 35 | descriptors = {} 36 | for d in parse_volume_descriptors(f): 37 | descriptors[d.dtype] = d 38 | try: 39 | primary = descriptors[VolumeDescriptor.Type.PRIMARY] 40 | except KeyError: 41 | raise FormatError('primary volume descriptor not found') 42 | walker = TreeWalker(f, primary) 43 | yield walker 44 | 45 | 46 | class VolumeDescriptor(typing.NamedTuple): 47 | 48 | class Type(enum.Enum): 49 | BOOT_RECORD = 0 50 | PRIMARY = 1 51 | SUPPLEMENTARY = 2 52 | PARTITION = 3 53 | TERMINATOR = 255 54 | 55 | dtype: int # 1 56 | identifier: bytes # 5 57 | version: int # 1 58 | data: bytes # 2041 59 | 60 | def __repr__(self): 61 | return f'VolumeDescriptor(dtype={self.dtype})' 62 | 63 | 64 | def parse_volume_descriptors(f): 65 | while True: 66 | d = parse_volume_descriptor(f) 67 | if d.dtype == VolumeDescriptor.Type.TERMINATOR: 68 | break 69 | yield d 70 | 71 | 72 | def parse_volume_descriptor(f): 73 | block = f.read(2048) 74 | if len(block) != 2048: 75 | raise FormatError( 76 | f'truncated volume descriptor: {len(block)} bytes') 77 | d = VolumeDescriptor( 78 | dtype=VolumeDescriptor.Type(block[0]), 79 | identifier=block[1:6], 80 | version=block[6], 81 | data=block[7:], 82 | ) 83 | if d.identifier != b'CD001': 84 | raise FormatError('bad volume descriptor identifier: {d.identifier!r}') 85 | if d.version != 1: 86 | raise FormatError('bad volume descriptor version: {d.version!r}') 87 | if d.dtype == VolumeDescriptor.Type.PRIMARY: 88 | return parse_primary_volume_descriptor(d) 89 | return d 90 | 91 | 92 | class PrimaryVolumeDescriptor(typing.NamedTuple): 93 | 94 | dtype: int # 1 95 | identifier: bytes # 5 96 | version: int # 1 97 | 98 | FORMAT = ( 99 | '{format}', struct.pack(f'<{format}', byteswapped)) 194 | if value != byteswapped: 195 | raise FormatError(f'{fieldname} mismatch: {value} != {byteswapped}') 196 | 197 | 198 | class DirectoryRecord(typing.NamedTuple): 199 | 200 | class Flags(enum.IntFlag): 201 | HIDDEN = 1 << 0 202 | DIRECTORY = 1 << 1 203 | ASSOCIATED_FILE = 1 << 2 204 | RECORD_FORMAT_SPECIFIED = 1 << 3 205 | PERMISSIONS_SPECIFIED = 1 << 4 206 | NOT_FINAL = 1 << 7 207 | 208 | FORMAT = ' pathlib.PurePosixPath: 301 | path = pathlib.PurePosixPath(path) 302 | if not path.is_absolute(): 303 | path = pathlib.PurePosixPath('/' + str(path)) 304 | if path not in self.cache: 305 | parent = self.lookup(path.parent) 306 | for entry in self.listdir(parent): 307 | self.cache.setdefault(parent / entry.name, entry) 308 | alt_paths = [path, str(path).upper()] 309 | if ';' not in path.name: 310 | alt_path = str(path) + ';1' 311 | alt_paths += [alt_path, alt_path.upper()] 312 | for path in map(pathlib.PurePosixPath, alt_paths): 313 | if path in self.cache: 314 | return path 315 | raise FileNotFoundError(f'no such file or directory: {path}') 316 | 317 | def get(self, path) -> DirectoryRecord: 318 | path = self.lookup(path) 319 | return self.cache[path] 320 | 321 | def listdir(self, path) -> typing.Iterator[DirectoryRecord]: 322 | d = self.get(path) 323 | if not d.is_directory: 324 | raise NotADirectoryError(f'not a directory: {path}') 325 | return parse_directory(d.read_from(self.f)) 326 | 327 | def read(self, path) -> bytes: 328 | d = self.get(path) 329 | if d.is_directory: 330 | raise IsADirectoryError(f'not a regular file: {path}') 331 | if d.has_extents: 332 | raise IsADirectoryError(f'{path} has multiple extents which is not supported') 333 | return d.read_from(self.f) 334 | 335 | 336 | def main(): 337 | parser = argparse.ArgumentParser( 338 | description="extact grub.cfg from an ISO image") 339 | parser.add_argument( 340 | "isofile", nargs='+', help="the ISO image file you want to inspect") 341 | parser.add_argument( 342 | "--path", default='/boot/grub/grub.cfg', 343 | help="filename of the file you want to extract (default: %(default)s)") 344 | parser.add_argument( 345 | "--ls", metavar='PATH', help="list the contents of this directory") 346 | args = parser.parse_args() 347 | for n, filename in enumerate(args.isofile): 348 | if len(args.isofile) > 1: 349 | if n > 0: 350 | print() 351 | print(f'#\n# {filename}\n#') 352 | try: 353 | with parse_iso(filename) as walker: 354 | if args.ls: 355 | for entry in walker.listdir(args.ls): 356 | print(entry.name + ('/' if entry.is_directory else '')) 357 | else: 358 | sys.stdout.buffer.write(walker.read(args.path)) 359 | except (OSError, FormatError) as e: 360 | print(e, file=sys.stderr) 361 | 362 | 363 | if __name__ == "__main__": 364 | main() 365 | --------------------------------------------------------------------------------