(* 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 Printf

open Std_utils

open Mountable
open Inspect_types
open Inspect_utils

let rec check_for_filesystem_on mountable vfs_type =
  if verbose () then
    eprintf "check_for_filesystem_on: %s (%s)\n%!"
            (Mountable.to_string mountable) vfs_type;

  let role =
    let is_swap = vfs_type = "swap" in
    if is_swap then
      Some RoleSwap
    else (
      (* Try mounting the device.  Ignore errors if we can't do this. *)
      let mounted =
        if vfs_type = "ufs" then ( (* Hack for the *BSDs. *)
          (* FreeBSD fs is a variant of ufs called ufs2 ... *)
          try
            Mount.mount_vfs "ro,ufstype=ufs2" "ufs" mountable "/";
            true
          with _ ->
            (* while NetBSD and OpenBSD use another variant labeled 44bsd *)
            try
              Mount.mount_vfs "ro,ufstype=44bsd" "ufs" mountable "/";
              true
            with _ -> false
        ) else (
          try Mount.mount_ro mountable "/";
              true
          with _ -> false
        ) in
      if not mounted then None
      else (
        let role = check_filesystem mountable in
        Mount_utils.umount_all ();
        role
      )
    ) in

  match role with
  | None -> None
  | Some role ->
     Some { fs_location = { mountable ; vfs_type }; role }

(* When this function is called, the filesystem is mounted on sysroot (). *)
and check_filesystem mountable =
  let role = ref `Other in
  (* The following struct is mutated in place by callees.  However we
   * need to make a copy of the object here so we don't mutate the
   * null_inspection_data struct!
   *)
  let data = null_inspection_data () in

  let debug_matching what =
    if verbose () then
      eprintf "check_filesystem: %s matched %s\n%!"
              (Mountable.to_string mountable) what
  in

  (* Grub /boot? *)
  if Is.is_file "/grub/menu.lst" ||
     Is.is_file "/grub/grub.conf" ||
     Is.is_file "/grub2/grub.cfg" then (
    debug_matching "Grub /boot";
    ()
  )
  (* FreeBSD root? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/bin" &&
          Is.is_file "/etc/freebsd-update.conf" &&
          Is.is_file "/etc/fstab" then (
    debug_matching "FreeBSD root";
    role := `Root;
    Inspect_fs_unix.check_freebsd_root mountable data
  )
  (* NetBSD root? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/bin" &&
          Is.is_file "/netbsd" &&
          Is.is_file "/etc/fstab" &&
          Is.is_file "/etc/release" then (
    debug_matching "NetBSD root";
    role := `Root;
    Inspect_fs_unix.check_netbsd_root mountable data;
  )
  (* OpenBSD root? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/bin" &&
          Is.is_file "/bsd" &&
          Is.is_file "/etc/fstab" &&
          Is.is_file "/etc/motd" then (
    debug_matching "OpenBSD root";
    role := `Root;
    Inspect_fs_unix.check_openbsd_root mountable data;
  )
  (* Hurd root? *)
  else if Is.is_file "/hurd/console" &&
          Is.is_file "/hurd/hello" &&
          Is.is_file "/hurd/null" then (
    debug_matching "Hurd root";
    role := `Root;
    Inspect_fs_unix.check_hurd_root mountable data;
  )
  (* Minix root? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/bin" &&
          Is.is_file "/service/vm" &&
          Is.is_file "/etc/fstab" &&
          Is.is_file "/etc/version" then (
    debug_matching "Minix root";
    role := `Root;
    Inspect_fs_unix.check_minix_root data;
  )
  (* Linux root? *)
  else if Is.is_dir "/etc" &&
          (Is.is_dir "/bin" ||
           is_symlink_to "/bin" "usr/bin") &&
          (Is.is_file "/etc/fstab" ||
           Is.is_file "/etc/hosts") then (
    debug_matching "Linux root";
    role := `Root;
    Inspect_fs_unix.check_linux_root mountable data;
  )
  (* CoreOS root? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/root" &&
          Is.is_dir "/home" &&
          Is.is_dir "/usr" &&
          Is.is_file "/etc/coreos/update.conf" then (
    debug_matching "CoreOS root";
    role := `Root;
    Inspect_fs_unix.check_coreos_root mountable data;
  )
  (* Linux /usr/local? *)
  else if Is.is_dir "/etc" &&
          Is.is_dir "/bin" &&
          Is.is_dir "/share" &&
          not (Is.is_dir "/local") &&
          not (Is.is_file "/etc/fstab") then (
    debug_matching "Linux /usr/local";
    ()
  )
  (* Linux /usr? *)
  else if Is.is_dir "/bin" &&
          Is.is_dir "/local" &&
          Is.is_dir "/share" &&
          Is.is_dir "/src" &&
          not (Is.is_file "/etc/fstab") then (
    debug_matching "Linux /usr";
    role := `Usr;
    Inspect_fs_unix.check_linux_usr data;
  )
  (* CoreOS /usr? *)
  else if Is.is_dir "/bin" &&
          Is.is_dir "/share" &&
          Is.is_dir "/local" &&
          Is.is_dir "/share/coreos" then (
    debug_matching "CoreOS /usr";
    role := `Usr;
    Inspect_fs_unix.check_coreos_usr mountable data;
  )
  (* Linux /var? *)
  else if Is.is_dir "/log" &&
          Is.is_dir "/run" &&
          Is.is_dir "/spool" then (
    debug_matching "Linux /var";
    ()
  )
  (* Windows root? *)
  else if Inspect_fs_windows.is_windows_systemroot () then (
    debug_matching "Windows root";
    role := `Root;
    Inspect_fs_windows.check_windows_root data;
  )
  (* Windows volume with installed applications (but not root)? *)
  else if is_dir_nocase "/System Volume Information" &&
          is_dir_nocase "/Program Files" then (
    debug_matching "Windows volume with installed applications";
    ()
  )
  (* Windows volume (but not root)? *)
  else if is_dir_nocase "/System Volume Information" then (
    debug_matching "Windows volume without installed applications";
    ()
  )
  (* FreeDOS? *)
  else if is_dir_nocase "/FDOS" &&
          is_file_nocase "/FDOS/FREEDOS.BSS" then (
    debug_matching "FreeDOS";
    role := `Root;
    data.os_type <- Some OS_TYPE_DOS;
    data.distro <- Some DISTRO_FREEDOS;
    (* FreeDOS is a mix of 16 and 32 bit, but
     * assume it requires a 32 bit i386 processor.
     *)
    data.arch <- Some "i386"
  )
  (* MS-DOS? *)
  else if is_dir_nocase "/DOS" &&
          is_file_nocase "/DOS/COMMAND.COM" then (
    debug_matching "MS-DOS";
    role := `Root;
    data.os_type <- Some OS_TYPE_DOS;
    data.distro <- Some DISTRO_MSDOS;
    (* MS-DOS is a mix of 16 and 32 bit, but
     * assume it requires a 32 bit i386 processor.
     *)
    data.arch <- Some "i386"
  )
  (* None of the above. *)
  else (
    debug_matching "no known OS partition"
  );

  (* The above code should have set [data.os_type] and [data.distro]
   * fields, so we can now guess the package management system.
   *)
  data.package_format <- check_package_format data;
  data.package_management <- check_package_management data;

  match !role with
  | `Root -> Some (RoleRoot data)
  | `Usr -> Some (RoleUsr data)
  | `Other -> Some RoleOther

and is_symlink_to file wanted_target =
  if not (Is.is_symlink file) then false
  else Link.readlink file = wanted_target

(* At the moment, package format and package management are just a
 * simple function of the [distro] and [version[0]] fields, so these
 * can never return an error.  We might be cleverer in future.
 *)
and check_package_format { distro } =
  match distro with
  | None -> None
  | Some DISTRO_ALTLINUX
  | Some DISTRO_CENTOS
  | Some DISTRO_CIRCLE
  | Some DISTRO_FEDORA
  | Some DISTRO_KYLIN
  | Some DISTRO_MAGEIA
  | Some DISTRO_MANDRIVA
  | Some DISTRO_MEEGO
  | Some DISTRO_NEOKYLIN
  | Some DISTRO_OPENCLOUDOS
  | Some DISTRO_OPENEULER
  | Some DISTRO_OPENMANDRIVA
  | Some DISTRO_OPENSUSE
  | Some DISTRO_ORACLE_LINUX
  | Some DISTRO_REDHAT_BASED
  | Some DISTRO_RHEL
  | Some DISTRO_ROCKY
  | Some DISTRO_SCIENTIFIC_LINUX
  | Some DISTRO_SLES
  | Some DISTRO_SUSE_BASED ->
     Some PACKAGE_FORMAT_RPM
  | Some DISTRO_DEBIAN
  | Some DISTRO_KALI_LINUX
  | Some DISTRO_LINUX_MINT
  | Some DISTRO_UBUNTU ->
     Some PACKAGE_FORMAT_DEB
  | Some DISTRO_ARCHLINUX ->
     Some PACKAGE_FORMAT_PACMAN
  | Some DISTRO_GENTOO ->
     Some PACKAGE_FORMAT_EBUILD
  | Some DISTRO_PARDUS ->
     Some PACKAGE_FORMAT_PISI
  | Some DISTRO_ALPINE_LINUX ->
     Some PACKAGE_FORMAT_APK
  | Some DISTRO_VOID_LINUX ->
     Some PACKAGE_FORMAT_XBPS
  | Some DISTRO_BUILDROOT
  | Some DISTRO_CIRROS
  | Some DISTRO_COREOS
  | Some DISTRO_FREEBSD
  | Some DISTRO_FREEDOS
  | Some DISTRO_FRUGALWARE
  | Some DISTRO_MSDOS
  | Some DISTRO_NETBSD
  | Some DISTRO_OPENBSD
  | Some DISTRO_PLD_LINUX
  | Some DISTRO_SLACKWARE
  | Some DISTRO_TTYLINUX
  | Some DISTRO_WINDOWS ->
     None

and check_package_management { distro; version } =
  let major = match version with None -> 0 | Some (major, _) -> major in
  match distro with
  | None -> None

  | Some DISTRO_MEEGO ->
     Some PACKAGE_MANAGEMENT_YUM

  | Some DISTRO_FEDORA ->
    (* If Fedora >= 22 and dnf is installed, say "dnf". *)
     if major >= 22 && Is.is_file ~followsymlinks:true "/usr/bin/dnf" then
       Some PACKAGE_MANAGEMENT_DNF
     else if major >= 1 then
       Some PACKAGE_MANAGEMENT_YUM
     else
       (* Probably parsing the release file failed, see RHBZ#1332025. *)
       None

  | Some DISTRO_NEOKYLIN ->
     (* We don't have access to NeoKylin for testing, but it is
      * supposed to be a Fedora derivative.
      *)
     Some PACKAGE_MANAGEMENT_DNF

  | Some DISTRO_CENTOS
  | Some DISTRO_CIRCLE
  | Some DISTRO_KYLIN
  | Some DISTRO_ORACLE_LINUX
  | Some DISTRO_REDHAT_BASED
  | Some DISTRO_RHEL
  | Some DISTRO_ROCKY
  | Some DISTRO_SCIENTIFIC_LINUX ->
     if major >= 8 then
       Some PACKAGE_MANAGEMENT_DNF
     else if major >= 5 then
       Some PACKAGE_MANAGEMENT_YUM
     else if major >= 2 then
       Some PACKAGE_MANAGEMENT_UP2DATE
     else
       (* Probably parsing the release file failed, see RHBZ#1332025. *)
       None

  | Some DISTRO_ALTLINUX
  | Some DISTRO_DEBIAN
  | Some DISTRO_KALI_LINUX
  | Some DISTRO_LINUX_MINT
  | Some DISTRO_UBUNTU ->
     Some PACKAGE_MANAGEMENT_APT

  | Some DISTRO_ARCHLINUX ->
     Some PACKAGE_MANAGEMENT_PACMAN

  | Some DISTRO_GENTOO ->
     Some PACKAGE_MANAGEMENT_PORTAGE

  | Some DISTRO_PARDUS ->
     Some PACKAGE_MANAGEMENT_PISI

  | Some DISTRO_MAGEIA
  | Some DISTRO_MANDRIVA ->
     Some PACKAGE_MANAGEMENT_URPMI

  | Some DISTRO_OPENSUSE
  | Some DISTRO_SLES
  | Some DISTRO_SUSE_BASED ->
     Some PACKAGE_MANAGEMENT_ZYPPER

  | Some DISTRO_ALPINE_LINUX ->
     Some PACKAGE_MANAGEMENT_APK

  | Some DISTRO_VOID_LINUX ->
     Some PACKAGE_MANAGEMENT_XBPS

  | Some DISTRO_OPENMANDRIVA ->
     Some PACKAGE_MANAGEMENT_DNF

  | Some DISTRO_OPENCLOUDOS ->
     Some PACKAGE_MANAGEMENT_DNF

  | Some DISTRO_OPENEULER->
     Some PACKAGE_MANAGEMENT_DNF

  | Some DISTRO_BUILDROOT
  | Some DISTRO_CIRROS
  | Some DISTRO_COREOS
  | Some DISTRO_FREEBSD
  | Some DISTRO_FREEDOS
  | Some DISTRO_FRUGALWARE
  | Some DISTRO_MSDOS
  | Some DISTRO_NETBSD
  | Some DISTRO_OPENBSD
  | Some DISTRO_PLD_LINUX
  | Some DISTRO_SLACKWARE
  | Some DISTRO_TTYLINUX
  | Some DISTRO_WINDOWS ->
    None

