libcoreinst/live/
mod.rs

1// Copyright 2019 CoreOS, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::{bail, Context, Result};
16use lazy_static::lazy_static;
17use serde::Serialize;
18use std::collections::HashMap;
19use std::fs::{create_dir_all, read, File, OpenOptions};
20use std::io::{self, copy, BufReader, BufWriter, Read, Seek, Write};
21use std::path::{Component, Path, PathBuf};
22
23use crate::cmdline::*;
24use crate::io::*;
25use crate::iso9660::{self, IsoFs};
26use crate::miniso;
27use crate::util::set_die_on_sigpipe;
28
29mod customize;
30mod embed;
31mod util;
32
33use self::customize::*;
34use self::embed::*;
35use self::util::*;
36
37const INITRD_LIVE_STAMP_PATH: &str = "etc/coreos-live-initramfs";
38const COREOS_ISO_PXEBOOT_DIR: &str = "IMAGES/PXEBOOT";
39const COREOS_ISO_ROOTFS_IMG: &str = "IMAGES/PXEBOOT/ROOTFS.IMG";
40const COREOS_ISO_MINISO_FILE: &str = "COREOS/MINISO.DAT";
41
42lazy_static! {
43    static ref ALL_GLOB: GlobMatcher = GlobMatcher::new(&["*"]).unwrap();
44}
45
46pub fn iso_embed(config: IsoEmbedConfig) -> Result<()> {
47    eprintln!("`iso embed` is deprecated; use `iso ignition embed`.  Continuing.");
48    iso_ignition_embed(IsoIgnitionEmbedConfig {
49        force: config.force,
50        ignition_file: config.config,
51        output: config.output,
52        input: config.input,
53    })
54}
55
56pub fn iso_show(config: IsoShowConfig) -> Result<()> {
57    eprintln!("`iso show` is deprecated; use `iso ignition show`.  Continuing.");
58    iso_ignition_show(IsoIgnitionShowConfig {
59        input: config.input,
60    })
61}
62
63pub fn iso_remove(config: IsoRemoveConfig) -> Result<()> {
64    eprintln!("`iso remove` is deprecated; use `iso ignition remove`.  Continuing.");
65    iso_ignition_remove(IsoIgnitionRemoveConfig {
66        output: config.output,
67        input: config.input,
68    })
69}
70
71pub fn iso_ignition_embed(config: IsoIgnitionEmbedConfig) -> Result<()> {
72    let ignition = match &config.ignition_file {
73        Some(ignition_path) => {
74            read(ignition_path).with_context(|| format!("reading {ignition_path}"))?
75        }
76        None => {
77            let mut data = Vec::new();
78            io::stdin()
79                .lock()
80                .read_to_end(&mut data)
81                .context("reading stdin")?;
82            data
83        }
84    };
85
86    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
87    let mut iso = IsoConfig::for_file(&mut iso_file)?;
88
89    if !config.force && iso.have_ignition() {
90        bail!("This ISO image already has an embedded Ignition config; use -f to force.");
91    }
92
93    iso.initrd_mut().add(INITRD_IGNITION_PATH, ignition);
94
95    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
96}
97
98pub fn iso_ignition_show(config: IsoIgnitionShowConfig) -> Result<()> {
99    set_die_on_sigpipe()?;
100    let mut iso_file = open_live_iso(&config.input, None)?;
101    let iso = IsoConfig::for_file(&mut iso_file)?;
102    if !iso.have_ignition() {
103        bail!("No embedded Ignition config.");
104    }
105    let stdout = io::stdout();
106    let mut out = stdout.lock();
107    out.write_all(
108        iso.initrd()
109            .get(INITRD_IGNITION_PATH)
110            .context("couldn't find Ignition config in archive")?,
111    )
112    .context("writing output")?;
113    out.flush().context("flushing output")?;
114    Ok(())
115}
116
117pub fn iso_ignition_remove(config: IsoIgnitionRemoveConfig) -> Result<()> {
118    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
119    let mut iso = IsoConfig::for_file(&mut iso_file)?;
120
121    iso.initrd_mut().remove(INITRD_IGNITION_PATH);
122
123    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
124}
125
126pub fn iso_network_embed(config: IsoNetworkEmbedConfig) -> Result<()> {
127    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
128    let mut iso_fs = IsoFs::from_file(iso_file.try_clone().context("cloning file")?)
129        .context("parsing ISO9660 image")?;
130    let mut iso = IsoConfig::for_iso(&mut iso_fs)?;
131
132    if !OsFeatures::for_iso(&mut iso_fs)?.live_initrd_network {
133        bail!("This OS image does not support customizing network settings.");
134    }
135    if !config.force && iso.have_network() {
136        bail!("This ISO image already has embedded network settings; use -f to force.");
137    }
138
139    iso.remove_network();
140    initrd_network_embed(iso.initrd_mut(), &config.keyfile)?;
141
142    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
143}
144
145pub fn iso_network_extract(config: IsoNetworkExtractConfig) -> Result<()> {
146    let mut iso_file = open_live_iso(&config.input, None)?;
147    let iso = IsoConfig::for_file(&mut iso_file)?;
148    initrd_network_extract(iso.initrd(), config.directory.as_ref())
149}
150
151pub fn iso_network_remove(config: IsoNetworkRemoveConfig) -> Result<()> {
152    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
153    let mut iso = IsoConfig::for_file(&mut iso_file)?;
154
155    iso.remove_network();
156
157    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
158}
159
160pub fn pxe_ignition_wrap(config: PxeIgnitionWrapConfig) -> Result<()> {
161    if config.output.is_none() {
162        verify_stdout_not_tty()?;
163    }
164
165    let ignition = match &config.ignition_file {
166        Some(ignition_path) => {
167            read(ignition_path).with_context(|| format!("reading {ignition_path}"))?
168        }
169        None => {
170            let mut data = Vec::new();
171            io::stdin()
172                .lock()
173                .read_to_end(&mut data)
174                .context("reading stdin")?;
175            data
176        }
177    };
178
179    let mut initrd = Initrd::default();
180    initrd.add(INITRD_IGNITION_PATH, ignition);
181
182    write_live_pxe(&initrd, config.output.as_ref())
183}
184
185pub fn pxe_ignition_unwrap(config: PxeIgnitionUnwrapConfig) -> Result<()> {
186    set_die_on_sigpipe()?;
187    let stdin = io::stdin();
188    let mut f: Box<dyn Read> = if let Some(path) = &config.input {
189        Box::new(
190            OpenOptions::new()
191                .read(true)
192                .open(path)
193                .with_context(|| format!("opening {path}"))?,
194        )
195    } else {
196        Box::new(stdin.lock())
197    };
198    let stdout = io::stdout();
199    let mut out = stdout.lock();
200    out.write_all(
201        Initrd::from_reader_filtered(&mut f, &INITRD_IGNITION_GLOB)?
202            .get(INITRD_IGNITION_PATH)
203            .context("couldn't find Ignition config in archive")?,
204    )
205    .context("writing output")?;
206    out.flush().context("flushing output")?;
207    Ok(())
208}
209
210pub fn pxe_network_wrap(config: PxeNetworkWrapConfig) -> Result<()> {
211    if config.output.is_none() {
212        verify_stdout_not_tty()?;
213    }
214
215    let mut initrd = Initrd::default();
216    initrd_network_embed(&mut initrd, &config.keyfile)?;
217
218    write_live_pxe(&initrd, config.output.as_ref())
219}
220
221fn initrd_network_embed(initrd: &mut Initrd, keyfiles: &[String]) -> Result<()> {
222    for path in keyfiles {
223        let data = read(path).with_context(|| format!("reading {path}"))?;
224        let name = filename(path)?;
225        let path = format!("{INITRD_NETWORK_DIR}/{name}");
226        if initrd.get(&path).is_some() {
227            bail!("multiple input files named '{}'", name);
228        }
229        initrd.add(&path, data);
230    }
231    Ok(())
232}
233
234pub fn pxe_network_unwrap(config: PxeNetworkUnwrapConfig) -> Result<()> {
235    let stdin = io::stdin();
236    let f: Box<dyn Read> = if let Some(path) = &config.input {
237        Box::new(
238            OpenOptions::new()
239                .read(true)
240                .open(path)
241                .with_context(|| format!("opening {path}"))?,
242        )
243    } else {
244        Box::new(stdin.lock())
245    };
246    initrd_network_extract(
247        &Initrd::from_reader_filtered(f, &INITRD_NETWORK_GLOB)?,
248        config.directory.as_ref(),
249    )
250}
251
252fn initrd_network_extract(initrd: &Initrd, directory: Option<&String>) -> Result<()> {
253    let files = initrd.find(&INITRD_NETWORK_GLOB);
254    if files.is_empty() {
255        bail!("No embedded network settings.");
256    }
257    if let Some(dir) = directory {
258        create_dir_all(dir)?;
259        for (path, contents) in files {
260            let path = Path::new(dir).join(filename(path)?);
261            OpenOptions::new()
262                .create_new(true)
263                .write(true)
264                .open(&path)
265                .with_context(|| format!("opening {}", path.display()))?
266                .write_all(contents)
267                .with_context(|| format!("writing {}", path.display()))?;
268            println!("{}", path.display());
269        }
270    } else {
271        set_die_on_sigpipe()?;
272        for (i, (path, contents)) in files.iter().enumerate() {
273            if i > 0 {
274                println!();
275            }
276            println!("########## {} ##########", filename(path)?);
277            io::stdout()
278                .lock()
279                .write_all(contents)
280                .context("writing network settings to stdout")?;
281        }
282    }
283    Ok(())
284}
285
286pub fn iso_kargs_modify(config: IsoKargsModifyConfig) -> Result<()> {
287    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
288    let mut iso = IsoConfig::for_file(&mut iso_file)?;
289
290    let kargs = KargsEditor::new()
291        .append(&config.append)
292        .replace(&config.replace)
293        .delete(&config.delete)
294        .apply_to(iso.kargs()?)?;
295    iso.set_kargs(&kargs)?;
296
297    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
298}
299
300pub fn iso_kargs_reset(config: IsoKargsResetConfig) -> Result<()> {
301    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
302    let mut iso = IsoConfig::for_file(&mut iso_file)?;
303
304    #[allow(clippy::unnecessary_to_owned)]
305    iso.set_kargs(&iso.kargs_default()?.to_string())?;
306
307    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
308}
309
310pub fn iso_kargs_show(config: IsoKargsShowConfig) -> Result<()> {
311    set_die_on_sigpipe()?;
312    let mut iso_file = open_live_iso(&config.input, None)?;
313    let iso = IsoConfig::for_file(&mut iso_file)?;
314    let kargs = if config.default {
315        iso.kargs_default()?
316    } else {
317        iso.kargs()?
318    };
319    println!("{kargs}");
320    Ok(())
321}
322
323pub fn iso_customize(config: IsoCustomizeConfig) -> Result<()> {
324    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
325    let mut iso_fs = IsoFs::from_file(iso_file.try_clone().context("cloning file")?)
326        .context("parsing ISO9660 image")?;
327    let mut iso = IsoConfig::for_iso(&mut iso_fs)?;
328
329    if !config.force
330        && (iso.have_ignition()
331            || iso.have_network()
332            || (iso.kargs_supported() && iso.kargs()? != iso.kargs_default()?))
333    {
334        bail!("This ISO image is already customized; use -f to force.");
335    }
336
337    let live = LiveInitrd::from_common(&config.common, OsFeatures::for_iso(&mut iso_fs)?)?;
338    *iso.initrd_mut() = live.into_initrd()?;
339
340    if [
341        &config.live_karg_append,
342        &config.live_karg_replace,
343        &config.live_karg_delete,
344    ]
345    .iter()
346    .any(|v| !v.is_empty())
347    {
348        if !iso.kargs_supported() {
349            bail!("This OS image does not support customizing live kernel arguments.");
350        }
351        let kargs = KargsEditor::new()
352            .append(&config.live_karg_append)
353            .replace(&config.live_karg_replace)
354            .delete(&config.live_karg_delete)
355            .apply_to(iso.kargs_default()?)?;
356        iso.set_kargs(&kargs)?;
357    }
358
359    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
360}
361
362pub fn iso_reset(config: IsoResetConfig) -> Result<()> {
363    let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
364    let mut iso = IsoConfig::for_file(&mut iso_file)?;
365
366    *iso.initrd_mut() = Initrd::default();
367    if iso.kargs_supported() {
368        #[allow(clippy::unnecessary_to_owned)]
369        iso.set_kargs(&iso.kargs_default()?.to_string())?;
370    };
371
372    write_live_iso(&iso, &mut iso_file, config.output.as_ref())
373}
374
375pub fn pxe_customize(config: PxeCustomizeConfig) -> Result<()> {
376    // open input and set up output
377    let mut input = BufReader::with_capacity(
378        BUFFER_SIZE,
379        OpenOptions::new()
380            .read(true)
381            .open(&config.input)
382            .with_context(|| format!("opening {}", &config.input))?,
383    );
384    let mut tempfile = match &*config.output {
385        "-" => {
386            verify_stdout_not_tty()?;
387            None
388        }
389        path => {
390            let dir = Path::new(path)
391                .parent()
392                .with_context(|| format!("no parent directory of {path}"))?;
393            let tempfile = tempfile::Builder::new()
394                .prefix(".coreos-installer-temp-")
395                .tempfile_in(dir)
396                .context("creating temporary file")?;
397            Some(tempfile)
398        }
399    };
400
401    // copy and check base initrd
402    let filter = GlobMatcher::new(&[
403        INITRD_LIVE_STAMP_PATH,
404        INITRD_FEATURES_PATH,
405        INITRD_IGNITION_PATH,
406        &format!("{INITRD_NETWORK_DIR}/*"),
407    ])
408    .unwrap();
409    let base_initrd = match &*config.output {
410        "-" => {
411            Initrd::from_reader_filtered(TeeReader::new(&mut input, io::stdout().lock()), &filter)
412                .context("reading/copying input initrd")?
413        }
414        _ => Initrd::from_reader_filtered(
415            TeeReader::new(&mut input, tempfile.as_mut().unwrap()),
416            &filter,
417        )
418        .context("reading/copying input initrd")?,
419    };
420    if base_initrd.get(INITRD_LIVE_STAMP_PATH).is_none() {
421        bail!("not a CoreOS live initramfs image");
422    }
423    if base_initrd.get(INITRD_IGNITION_PATH).is_some()
424        || !base_initrd.find(&INITRD_NETWORK_GLOB).is_empty()
425    {
426        bail!("input is already customized");
427    }
428    let features = match base_initrd.get(INITRD_FEATURES_PATH) {
429        Some(json) => serde_json::from_slice::<OsFeatures>(json).context("parsing OS features")?,
430        None => OsFeatures::default(),
431    };
432
433    let live = LiveInitrd::from_common(&config.common, features)?;
434    let initrd = live.into_initrd()?;
435    if initrd.get(INITRD_IGNITION_PATH).is_some() {
436        eprintln!(
437            "PXE configuration must include kernel arguments:\n\tignition.firstboot ignition.platform.id=metal"
438        );
439    }
440
441    // append customizations to output
442    let do_write = |writer: &mut dyn Write| -> Result<()> {
443        let mut buf = BufWriter::with_capacity(BUFFER_SIZE, writer);
444        buf.write_all(&initrd.to_bytes()?)
445            .context("writing initrd")?;
446        buf.flush().context("flushing initrd")
447    };
448    match &*config.output {
449        "-" => do_write(&mut io::stdout().lock()),
450        path => {
451            let mut tempfile = tempfile.unwrap();
452            do_write(tempfile.as_file_mut())?;
453            tempfile
454                .persist_noclobber(path)
455                .map_err(|e| e.error)
456                .with_context(|| format!("persisting output file to {path}"))?;
457            Ok(())
458        }
459    }
460}
461
462#[derive(Serialize)]
463struct DevShowIsoOutput {
464    header: IsoFs,
465    records: Vec<String>,
466}
467
468pub fn dev_show_iso(config: DevShowIsoConfig) -> Result<()> {
469    set_die_on_sigpipe()?;
470    let mut iso_file = open_live_iso(&config.input, None)?;
471    let stdout = io::stdout();
472    let mut out = stdout.lock();
473    if config.ignition || config.kargs {
474        let iso = IsoConfig::for_file(&mut iso_file)?;
475        let data = if config.ignition {
476            iso.initrd_header_json()?
477        } else {
478            iso.kargs_header_json()?
479        };
480        out.write_all(&data).context("failed to write header")?;
481    } else {
482        let mut iso = IsoFs::from_file(iso_file)?;
483        let records = iso
484            .walk()?
485            .map(|r| r.map(|(path, _)| path))
486            .collect::<Result<Vec<String>>>()
487            .context("while walking ISO filesystem")?;
488        let info = DevShowIsoOutput {
489            header: iso,
490            records,
491        };
492
493        serde_json::to_writer_pretty(&mut out, &info)
494            .context("failed to serialize ISO metadata")?;
495        out.write_all(b"\n").context("failed to write newline")?;
496    }
497    Ok(())
498}
499
500pub fn dev_show_initrd(config: DevShowInitrdConfig) -> Result<()> {
501    set_die_on_sigpipe()?;
502    let initrd = read_initrd(&config.input, &config.filter)?;
503    for path in initrd.find(&ALL_GLOB).keys() {
504        println!("{path}");
505    }
506    Ok(())
507}
508
509pub fn dev_extract_initrd(config: DevExtractInitrdConfig) -> Result<()> {
510    let initrd = read_initrd(&config.input, &config.filter)?;
511    let base_path = Path::new(&config.directory);
512    for (path, contents) in initrd.find(&ALL_GLOB) {
513        if Path::new(path)
514            .components()
515            .any(|c| matches!(c, Component::RootDir | Component::ParentDir))
516        {
517            bail!("path {} contains path traversal", path);
518        }
519        let out_path = base_path.join(path);
520        if config.verbose {
521            println!("{}", out_path.display());
522        }
523        let out_parent = out_path
524            .parent()
525            .with_context(|| format!("finding parent of {}", out_path.display()))?;
526        create_dir_all(out_parent).with_context(|| format!("creating {}", out_parent.display()))?;
527        OpenOptions::new()
528            .create_new(true)
529            .write(true)
530            .open(&out_path)
531            .with_context(|| format!("opening {}", out_path.display()))?
532            .write_all(contents)
533            .with_context(|| format!("writing {}", out_path.display()))?;
534    }
535    Ok(())
536}
537
538fn read_initrd(path: &str, filter: &[String]) -> Result<Initrd> {
539    let filter = if filter.is_empty() {
540        vec!["*"]
541    } else {
542        filter.iter().map(String::as_str).collect()
543    };
544    let filter = GlobMatcher::new(&filter).context("parsing glob patterns")?;
545    match path {
546        "-" => Initrd::from_reader_filtered(io::stdin().lock(), &filter),
547        path => Initrd::from_reader_filtered(
548            OpenOptions::new()
549                .read(true)
550                .open(path)
551                .with_context(|| format!("opening {path}"))?,
552            &filter,
553        ),
554    }
555    .context("decoding initrd")
556}
557
558pub fn iso_extract_pxe(config: IsoExtractPxeConfig) -> Result<()> {
559    let mut iso = IsoFs::from_file(open_live_iso(&config.input, None)?)?;
560    let pxeboot = iso
561        .get_path(COREOS_ISO_PXEBOOT_DIR)
562        .context("Unrecognized CoreOS ISO image.")?
563        .try_into_dir()?;
564    create_dir_all(&config.output_dir)?;
565
566    let base = {
567        // this can't be None since we successfully opened the live ISO at the location
568        let mut s = Path::new(&config.input).file_stem().unwrap().to_os_string();
569        s.push("-");
570        s
571    };
572
573    for record in iso.list_dir(&pxeboot)? {
574        match record? {
575            iso9660::DirectoryRecord::Directory(_) => continue,
576            iso9660::DirectoryRecord::File(file) => {
577                let filename = {
578                    let mut s = base.clone();
579                    s.push(file.name.to_lowercase());
580                    s
581                };
582                let path = Path::new(&config.output_dir).join(filename);
583                println!("{}", path.display());
584                copy_file_from_iso(&mut iso, &file, &path)?;
585            }
586        }
587    }
588    Ok(())
589}
590
591pub fn iso_extract_minimal_iso(config: IsoExtractMinimalIsoConfig) -> Result<()> {
592    // Note we don't support overwriting the input ISO. Unlike other commands, this operation is
593    // non-reversible, so let's make it harder for users to shoot themselves in the foot.
594    let mut full_iso = IsoFs::from_file(open_live_iso(&config.input, None)?)?;
595
596    // For now, we require the full ISO to be completely vanilla. Otherwise, the hashes won't
597    // match.
598    let iso = IsoConfig::for_iso(&mut full_iso)?;
599    if !iso.initrd().is_empty() || iso.kargs()? != iso.kargs_default()? {
600        bail!("Cannot operate on ISO with embedded customizations.\nReset it with `coreos-installer iso reset` and try again.");
601    }
602
603    // do this early so we exit immediately if stdout is a TTY
604    let output_dir: PathBuf = if &config.output == "-" {
605        verify_stdout_not_tty()?;
606        std::env::temp_dir()
607    } else {
608        Path::new(&config.output)
609            .parent()
610            .with_context(|| format!("no parent directory of {}", &config.output))?
611            .into()
612    };
613
614    if let Some(path) = &config.output_rootfs {
615        let rootfs = full_iso
616            .get_path(COREOS_ISO_ROOTFS_IMG)
617            .with_context(|| format!("looking up '{COREOS_ISO_ROOTFS_IMG}'"))?
618            .try_into_file()?;
619        copy_file_from_iso(&mut full_iso, &rootfs, Path::new(path))?;
620    }
621
622    let miniso_data_file = match full_iso.get_path(COREOS_ISO_MINISO_FILE) {
623        Ok(record) => record.try_into_file()?,
624        Err(e) if e.is::<iso9660::NotFound>() => {
625            bail!("This ISO image does not support extracting a minimal ISO.")
626        }
627        Err(e) => return Err(e).with_context(|| format!("looking up '{COREOS_ISO_MINISO_FILE}'")),
628    };
629
630    let data = {
631        let mut f = full_iso.read_file(&miniso_data_file)?;
632        miniso::Data::deserialize(&mut f).context("reading miniso data file")?
633    };
634    let mut outf = tempfile::Builder::new()
635        .prefix(".coreos-installer-temp-")
636        .tempfile_in(output_dir)
637        .context("creating temporary file")?;
638    data.unxzpack(full_iso.as_file()?, &mut outf)
639        .context("unpacking miniso")?;
640
641    modify_miniso_kargs(outf.as_file_mut(), config.rootfs_url.as_ref())
642        .context("modifying miniso kernel args")?;
643
644    if &config.output == "-" {
645        outf.rewind()
646            .context("seeking back to start of miniso tempfile")?;
647        copy(&mut outf, &mut io::stdout().lock()).context("writing output")?;
648    } else {
649        outf.persist_noclobber(&config.output)
650            .map_err(|e| e.error)?;
651    }
652
653    Ok(())
654}
655
656pub fn pack_minimal_iso(config: PackMinimalIsoConfig) -> Result<()> {
657    let mut full_iso = IsoFs::from_file(open_live_iso(&config.full, Some(None))?)?;
658    let mut minimal_iso = IsoFs::from_file(open_live_iso(&config.minimal, None)?)?;
659
660    let full_files = collect_iso_files(&mut full_iso)
661        .with_context(|| format!("collecting files from {}", &config.full))?;
662    let minimal_files = collect_iso_files(&mut minimal_iso)
663        .with_context(|| format!("collecting files from {}", &config.minimal))?;
664    if full_files.is_empty() {
665        bail!("No files found in {}", &config.full);
666    } else if minimal_files.is_empty() {
667        bail!("No files found in {}", &config.minimal);
668    }
669
670    eprintln!("Packing minimal ISO");
671    let (data, matches, skipped, written, written_compressed) =
672        miniso::Data::xzpack(minimal_iso.as_file()?, &full_files, &minimal_files)
673            .context("packing miniso")?;
674    eprintln!("Matched {} files of {}", matches, minimal_files.len());
675
676    eprintln!("Total bytes skipped: {skipped}");
677    eprintln!("Total bytes written: {written}");
678    eprintln!("Total bytes written (compressed): {written_compressed}");
679
680    eprintln!("Verifying that packed image matches digest");
681    data.unxzpack(full_iso.as_file()?, std::io::sink())
682        .context("unpacking miniso for verification")?;
683
684    let miniso_entry = full_iso
685        .get_path(COREOS_ISO_MINISO_FILE)
686        .with_context(|| format!("looking up '{COREOS_ISO_MINISO_FILE}'"))?
687        .try_into_file()?;
688    let mut w = full_iso.overwrite_file(&miniso_entry)?;
689    data.serialize(&mut w).context("writing miniso data file")?;
690    w.flush().context("flushing full ISO")?;
691
692    if config.consume {
693        std::fs::remove_file(&config.minimal)
694            .with_context(|| format!("consuming {}", &config.minimal))?;
695    }
696
697    eprintln!("Packing successful!");
698    Ok(())
699}
700
701fn collect_iso_files(iso: &mut IsoFs) -> Result<HashMap<String, iso9660::File>> {
702    iso.walk()?
703        .filter_map(|r| match r {
704            Err(e) => Some(Err(e)),
705            Ok((s, iso9660::DirectoryRecord::File(f))) => Some(Ok((s, f))),
706            Ok(_) => None,
707        })
708        .collect::<Result<HashMap<String, iso9660::File>>>()
709        .context("while walking ISO filesystem")
710}
711
712fn modify_miniso_kargs(f: &mut File, rootfs_url: Option<&String>) -> Result<()> {
713    let mut iso = IsoFs::from_file(f.try_clone().context("cloning a file")?)?;
714    let mut cfg = IsoConfig::for_file(f)?;
715
716    let kargs = cfg.kargs()?;
717
718    // same disclaimer as `modify_kargs()` here re. whitespace/quoting
719    let liveiso_karg = kargs
720        .split_ascii_whitespace()
721        .find(|&karg| karg.starts_with("coreos.liveiso="))
722        .context("minimal ISO does not have coreos.liveiso= karg")?
723        .to_string();
724
725    let new_default_kargs = KargsEditor::new().delete(&[liveiso_karg]).apply_to(kargs)?;
726    cfg.set_kargs(&new_default_kargs)?;
727
728    if let Some(url) = rootfs_url {
729        if url.split_ascii_whitespace().count() > 1 {
730            bail!("forbidden whitespace found in '{}'", url);
731        }
732        let final_kargs = KargsEditor::new()
733            .append(&[format!("coreos.live.rootfs_url={url}")])
734            .apply_to(&new_default_kargs)?;
735
736        cfg.set_kargs(&final_kargs)?;
737    }
738
739    // update kargs
740    write_live_iso(&cfg, f, None)?;
741
742    // also modify the default kargs because we don't want `coreos-installer iso kargs reset` to
743    // re-add `coreos.liveiso`
744    set_default_kargs(&mut iso, new_default_kargs)
745}