(* Parse isoinfo or xorriso output.
 * 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 Unix

open Std_utils

include Structs

(* We treat ISO "strA" and "strD" formats the same way, simply
 * discarding any trailing spaces.
 *)
let iso_parse_strA str =
  let len = String.length str in
  let rec loop len =
    if len > 0 && str.[len-1] = ' ' then loop (len-1) else len
  in
  let len = loop len in
  String.sub str 0 len

let iso_parse_strD = iso_parse_strA

(* These always parse the intX_LSB (little endian) version. *)
let iso_parse_int16 s = s |> int_of_le16 |> Int64.to_int32
let iso_parse_int32 s = s |> int_of_le32 |> Int64.to_int32

(* Parse ISO dec-datetime to a Unix time_t. *)
let iso_parse_datetime str =
  if String.sub str 0 16 = "0000000000000000" then -1_L
  else (
    let tm_year = int_of_string (String.sub str 0 4) in
    let tm_mon = int_of_string (String.sub str 4 2) in
    let tm_mday = int_of_string (String.sub str 6 2) in
    let tm_hour = int_of_string (String.sub str 8 2) in
    let tm_min = int_of_string (String.sub str 10 2) in
    let tm_sec = int_of_string (String.sub str 12 2) in

    (* Adjust fields. *)
    let tm_year = tm_year - 1900 in
    let tm_mon = tm_mon - 1 in

    (* Convert to time_t in UTC timezone. *)
    let tm = { tm_sec; tm_min; tm_hour; tm_mday; tm_mon; tm_year;
               tm_wday = -1; tm_yday = -1; tm_isdst = false } in
    let old_TZ = try Some (getenv "TZ") with Not_found -> None in
    putenv "TZ" "UTC";
    let r = Int64.of_float (fst (mktime tm)) in
    Option.iter (putenv "TZ") old_TZ;

    (* The final byte is a time zone offset from GMT.
     *
     * The documentation of this at
     * https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor
     * is wrong.  See the ECMA 119 documentation for a correct
     * description.
     *
     * For a disk image which we know was created
     * in BST (GMT+1), this contains 0x4, ie. 4 * 15 mins ahead.
     * We have to subtract this from the gmtime above.
     *)
    let tz = Char.code str.[16] in
    let tz = if tz >= 128 then tz - 256 else tz in
    r -^ (Int64.of_int (tz * 15 * 60))
  )

let isoinfo_device dev =
  let r, pvd =
    with_openfile dev [O_RDONLY] 0 (
        fun fd ->
        ignore (lseek fd 32768 SEEK_SET);
        let pvd = Bytes.create 2048 in
        let r = read fd pvd 0 2048 in
        r, Bytes.to_string pvd
      ) in

  let sub = String.sub pvd in

  (* Check that it looks like an ISO9660 Primary Volume Descriptor.
   * https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor
   *)
  if r <> 2048 || pvd.[0] <> '\001' || sub 1 5 <> "CD001" then
    failwithf "%s: not an ISO file or CD-ROM" dev;

  (* Parse out the PVD fields. *)
  let iso_system_id = sub 8 32 |> iso_parse_strA in
  let iso_volume_id = sub 40 32 |> iso_parse_strA in
  let iso_volume_space_size = sub 80 4 |> iso_parse_int32 in
  let iso_volume_set_size = sub 120 2 |> iso_parse_int16 in
  let iso_volume_sequence_number = sub 124 2 |> iso_parse_int16 in
  let iso_logical_block_size = sub 128 2 |> iso_parse_int16 in
  let iso_volume_set_id = sub 190 128 |> iso_parse_strD in
  let iso_publisher_id = sub 318 128 |> iso_parse_strA in
  let iso_data_preparer_id = sub 446 128 |> iso_parse_strA in
  let iso_application_id = sub 574 128 |> iso_parse_strA in
  let iso_copyright_file_id = sub 702 37 |> iso_parse_strD in
  let iso_abstract_file_id = sub 739 37 |> iso_parse_strD in
  let iso_bibliographic_file_id = sub 776 37 |> iso_parse_strD in
  let iso_volume_creation_t = sub 813 17 |> iso_parse_datetime in
  let iso_volume_modification_t = sub 830 17 |> iso_parse_datetime in
  let iso_volume_expiration_t = sub 847 17 |> iso_parse_datetime in
  let iso_volume_effective_t = sub 864 17 |> iso_parse_datetime in

  (* Return the struct. *)
  {
    iso_system_id; iso_volume_id; iso_volume_space_size;
    iso_volume_set_size; iso_volume_sequence_number;
    iso_logical_block_size; iso_volume_set_id; iso_publisher_id;
    iso_data_preparer_id; iso_application_id; iso_copyright_file_id;
    iso_abstract_file_id; iso_bibliographic_file_id;
    iso_volume_creation_t; iso_volume_modification_t;
    iso_volume_expiration_t; iso_volume_effective_t;
  }

let isoinfo file =
  let chroot = Chroot.create ~name:(sprintf "isoinfo: %s" file) () in
  Chroot.f chroot isoinfo_device file
