(* guestfs-inspection
 * Copyright (C) 2009-2025 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *)

open Unix
open Printf

open Std_utils
open Unix_utils

open Utils

let re_file_elf =
  PCRE.compile "ELF (\\d+)-bit (MSB|LSB).*(?:executable|shared object|relocatable)(?:,)? ([^,]+?),"

let re_file_elf_ppc64 = PCRE.compile ".*64.*PowerPC"

let initrd_binaries = [
  "bin/ls";
  "bin/rm";
  "bin/modprobe";
  "sbin/modprobe";
  "bin/sh";
  "bin/bash";
  "bin/dash";
  "bin/nash";
]

let rec file_architecture orig_path =
  (* Get the output of the "file" command.  Note that because this
   * is running in the daemon, LANG=C so it's in English.
   *)
  let magic = File.file orig_path in
  file_architecture_of_magic magic orig_path orig_path

and file_architecture_of_magic magic orig_path path =
  if PCRE.matches re_file_elf magic then (
    let bits = PCRE.sub 1 in
    let endianness = PCRE.sub 2 in
    let elf_arch = PCRE.sub 3 in
    canonical_elf_arch bits endianness elf_arch
  )
  else if String.find magic "PE32 executable" >= 0 then
    "i386"
  else if String.find magic "PE32+ executable" >= 0 then
    "x86_64"
  else if String.find magic "cpio archive" >= 0 then
    cpio_arch magic orig_path path
  else
    failwithf "unknown architecture: %s" path

(* Convert output from 'file' command on ELF files to the canonical
 * architecture string.
 *)
and canonical_elf_arch bits endianness elf_arch =
  let substr s = String.find elf_arch s >= 0 in
  if substr "Intel 80386" || substr "Intel i386" || substr "Intel 80486" then
    "i386"
  else if substr "x86-64" || substr "AMD x86-64" then
    "x86_64"
  else if substr "SPARC32" then
    "sparc"
  else if substr "SPARC V9" then
    "sparc64"
  else if substr "IA-64" then
    "ia64"
  else if PCRE.matches re_file_elf_ppc64 elf_arch then (
    match endianness with
    | "MSB" -> "ppc64"
    | "LSB" -> "ppc64le"
    | _ -> failwithf "unknown endianness '%s'" endianness
  )
  else if substr "PowerPC" then
    "ppc"
  else if substr "ARM aarch64" then
    "aarch64"
  else if substr "ARM" then
    "arm"
  else if substr "UCB RISC-V" then
    sprintf "riscv%s" bits
  else if substr "IBM S/390" then (
    match bits with
    | "32" -> "s390"
    | "64" -> "s390x"
    | _ -> failwithf "unknown S/390 bit size: %s" bits
  )
  else if substr "LoongArch" then
    sprintf "loongarch%s" bits
  else
    elf_arch

and cpio_arch magic orig_path path =
  let zcat =
    if String.find magic "gzip" >= 0 then "zcat"
    else if String.find magic "bzip2" >= 0 then "bzcat"
    else if String.find magic "XZ compressed" >= 0 then "xzcat"
    else if String.find magic "Zstandard compressed" >= 0 then "zstdcat"
    else "cat" in

  let tmpdir = Mkdtemp.temp_dir "filearch" in
  let finally () = ignore (Sys.command (sprintf "rm -rf %s" (quote tmpdir))) in

  protect ~finally ~f:(
    fun () ->
      (* Construct a command to extract named binaries from the initrd file. *)
      let cmd =
        sprintf "cd %s && %s %s | cpio --quiet -id %s"
                tmpdir zcat (quote (Sysroot.sysroot_path path))
                (String.concat " " (List.map quote initrd_binaries)) in
      if verbose () then eprintf "%s\n%!" cmd;
      if Sys.command cmd <> 0 then
        failwith "cpio command failed";

      (* See if any of the binaries were present in the output. *)
      let rec loop = function
        | bin :: bins ->
           let bin_path = tmpdir // bin in
           if is_regular_file bin_path then (
             let file_options =
               sprintf "-z%sb"
                 (if File_helper.file_has_S_option () then "S" else "") in
             let out = command "file" [file_options; bin_path] in
             file_architecture_of_magic out orig_path bin_path
           )
           else
             loop bins
        | [] ->
           failwithf "could not determine architecture of cpio archive: %s"
                     path
      in
      loop initrd_binaries
  )
