(* SELinux functions.
 * 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 Sysroot
open Utils

(* Test if setfiles has various options.
 *
 * The only way to do this is to run setfiles with the option alone, and
 * test for the stderr message [invalid option -- 'X'].
 *)
let setfiles_has_option =
  let test_setfiles flag =
    let err_msg = sprintf "invalid option -- '%c'" flag in
    let opt = sprintf "-%c" flag in
    let _, _, err = commandr "setfiles" [opt] in
    String.find err err_msg = -1
  in
  let h = Hashtbl.create 16 in
  fun flag ->
    try Hashtbl.find h flag
    with
    | Not_found ->
       let r = test_setfiles flag in
       Hashtbl.add h flag r;
       r

let setfiles ?(force = false) specfile paths =
  if paths = [] then ()
  else (
    (* Prefix /sysroot on all paths. *)
    let ignored_paths =
      [ "/dev"; "/proc"; "/selinux"; "/sys" ] |>
      List.map sysroot_path in
    let specfile = sysroot_path specfile in
    let paths = List.map sysroot_path paths in

    let args = ref [] in
    if force then List.push_back args "-F";
    List.iter (
      fun ignored_path ->
        List.push_back_list args [ "-e"; ignored_path ]
    ) ignored_paths;

    (* You have to use the -m option (where available) otherwise
     * setfiles puts all the mountpoints on the excludes list for no
     * useful reason (RHBZ#1433577).
     *)
    if setfiles_has_option 'm' then List.push_back args "-m";

    (* Not only do we want setfiles to trudge through individual relabeling
     * errors, we also want the setfiles exit status to differentiate a fatal
     * error from "relabeling errors only". See RHBZ#1794518.
     *)
    if setfiles_has_option 'C' then List.push_back args "-C";

    (* Use the -A option if available to reduce the amount of memory
     * used in filesystems with millions of files
     * See: https://issues.redhat.com/browse/RHEL-111505
     *)
    if setfiles_has_option 'A' then List.push_back args "-A";

    (* If the appliance is being run with multiple vCPUs, running setfiles
     * in multithreading mode might speed up the process.  Option "-T" was
     * introduced in SELinux userspace v3.4, and we need to check whether it's
     * supported.  Passing "-T 0" creates as many threads as there're available
     * vCPU cores.
     * https://github.com/SELinuxProject/selinux/releases/tag/3.4
     *)
    if setfiles_has_option 'T' then
      List.push_back_list args [ "-T"; "0" ];

    (* Relabelling in a chroot. *)
    if sysroot () <> "/" then
      List.push_back_list args [ "-r"; sysroot () ];

    if verbose () then
      List.push_back args "-v"
    else
      (* Suppress non-error output. *)
      List.push_back args "-q";

    (* Add parameters. *)
    List.push_back args specfile;
    List.push_back_list args paths;

    let args = !args in
    let r, _, err = commandr "setfiles" args in

    let ok = r = 0 || r = 1 && setfiles_has_option 'C' in
    if not ok then failwithf "setfiles: %s" err
  )

(* This is the deprecated selinux_relabel function from libguestfs <= 1.56. *)
let selinux_relabel ?force specfile path = setfiles ?force specfile [path]
