(* 'diskzip' command for intelligently compressing disk images. (C) Copyright 2007 Richard W.M. Jones, Red Hat Inc. http://libvirt.org/ 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., 675 Mass Ave, Cambridge, MA 02139, USA. *) open Unix open Printf open Diskzip_gettext.Gettext type output = File of string | Dir of string type extcompress = BZip2 | GZip | External of string let rec main () = (* Program name changes behaviour. *) let compressing = let name = Sys.argv.(0) in let name = Filename.basename name in (* just the executable name *) let name = Filename.chop_extension name in (* remove .opt or .exe *) let name = String.lowercase name in match name with | "diskzcat" -> false | "diskzip" -> true | name -> eprintf (f_"diskzip: unknown executable name '%s', assuming 'diskzip'\n") name in let compressing = ref compressing in (* Command line argument parsing. *) let version () = printf "diskzip\n"; (* XXX version XXX *) exit 0 in let output = ref None in let set_output path = if !output <> None then ( prerr_endline (s_"diskzip: '-o' option cannot appear more than once"); exit 2 ); try let statbuf = stat path in if statbuf.st_kind = S_DIR then output := Some (Dir path) else output := Some (File path) with (* No such file or directory, assume it's a file output. *) | Unix_error (ENOENT, _, _) -> output := Some (File path) in (* By default we don't use any external compression program. *) let extcompress = ref None in let set_extcompress t () = if !extcompress <> None then ( prerr_endline (s_"diskzip: '-z' or '-j' cannot appear more than once"); exit 2 ); extcompress := Some t in let force = ref false in let argspec = Arg.align [ "-d", Arg.Clear compressing, " " ^ s_ "Uncompress (default: depends on executable name)"; "--debug", Arg.Set Diskimage.debug, " " ^ s_ "Debug mode (default: false)"; "-f", Arg.Set force, " " ^ s_"Force compress even if stdout looks like a tty"; "-j", Arg.Unit (set_extcompress BZip2), " " ^ s_"Pipe the output/input through bzip2"; "-o", Arg.String set_output, "path " ^ s_"Set the output filename or directory name"; "-p", Arg.String (fun prog -> set_extcompress (External prog) ()), "prog " ^ s_"Pipe the output/input through external program"; "--version", Arg.Unit version, " " ^ s_"Display version and exit"; "-z", Arg.Unit (set_extcompress GZip), " " ^ s_"Pipe the output/input through gzip"; ] in let args = ref [] in let anon_fun str = args := str :: !args in let usage_msg = s_"diskzip: Intelligently compress disk images SUMMARY diskzip [-options] disk.img [disk.img ...] > output.dz diskzcat [-options] output.dz > disk.img OPTIONS" in Arg.parse argspec anon_fun usage_msg; (* Turn refs back into normal values. *) let compressing = !compressing in let extcompress = !extcompress in let output = !output in let force = !force in let args = !args in (* Check the arguments make sense. *) if compressing && output <> None then ( prerr_endline (s_"diskzip: '-o' option cannot be used when compressing"); exit 2 ); if compressing && args = [] then ( prerr_endline (s_"diskzip: no input"); exit 2 ); if compressing && not force && isatty stdout then ( prerr_endline (s_"diskzip: compressed data not written to a terminal, use '-f' to force"); exit 2 ); (* Run the compression or decompression functions. *) if compressing then go_compress extcompress args else go_decompress ?output extcompress args (* Do compression. *) and go_compress extcompress images = (* Create a Diskimage machine description from the requested images. This * also checks that everything we need is readable. *) let machine = Diskimage.open_machine "diskzip" (List.map (fun n -> (n,n)) images) in (* Scan the images for filesystems. *) let machine = Diskimage.scan_machine machine in (* Redirect output through external pipe if asked. *) (match extcompress with | None -> () | Some prog -> let prog, progargs = match prog with | BZip2 -> "bzip2", [|"bzip2"; "-c"|] | GZip -> "gzip", [|"gzip"; "-c"|] | External prog -> "sh", [|"sh"; "-c"; prog |] in let rfd, wfd = pipe () in let pid = fork () in if pid = 0 then ( (* child *) close wfd; dup2 rfd stdin; close rfd; execvp prog progargs ) else ( (* parent *) close rfd; dup2 wfd stdout; close wfd ) ) and go_decompress ?output extcompress args = (* Read the input, which may be a single named file, or a series of * files (we just concatenate them). We may have to feed the input * through an external program. *) let () = match args with | [] -> () (* Reading from stdin. *) | [file] -> (* Read the named file. *) let fd = openfile file [O_RDONLY] 0 in dup2 fd stdin; close fd | files -> (* Concatenate files. *) let rfd, wfd = pipe () in let pid = fork () in if pid = 0 then ( (* child *) close rfd; dup2 wfd stdout; close wfd; execvp "cat" (Array.of_list ("cat" :: "--" :: files)) ) else ( (* parent *) close wfd; dup2 rfd stdin; close rfd ) in (match extcompress with | None -> () | Some prog -> let prog, progargs = match prog with | BZip2 -> "bzip2", [|"bzip2"; "-cd"|] | GZip -> "gzip", [|"gzip"; "-cd"|] | External prog -> "sh", [|"sh"; "-c"; prog |] in let rfd, wfd = pipe () in let pid = fork () in if pid = 0 then ( (* child *) close rfd; dup2 wfd stdout; close wfd; execvp prog progargs ) else ( (* parent *) close wfd; dup2 rfd stdin; close rfd ) ) (* let header = read_header () in XXX *) (* (* Since we have the wonderful pa_bitmatch, might as well use it to * define a robust binary format for the compressed files. *) and write_header ... = let bs = BITSTRING { 0xD152 : 16; 0x01 : 8; 0x00 : 8; (* file magic, version 1.0 *) nr_disks : 8; (* number of disks being packed *) } in and read_header () = (* Diskzip headers are limited to overall max size of 1024 bytes. *) let bs = Bitmatch.bitstring_of_file_descr_max stdin 1024 in bitmatch bs with | { 0xD152 : 16; (* file magic *) 0x01 : 8; (_ as minor) : 8; (* major, minor versions *) } -> (* Is this a later version (major != 1)? *) | { 0xD152 : 16; (* file magic *) (_ as major) : 8; (_ as minor) : 8 } when major <> 1 -> eprintf (f_"diskzip: archive version %d.%d, this program only understands version 1.x") major minor; exit 1 (* If it looks like gzip or bzip2, exit with an informative error. *) | { 0o37 : 8; 0o213 : 8 } -> (* gzip *) prerr_endline (s_"diskzip: This looks like a gzip archive. Did you mean to pass the '-z' option?"); exit 1 | { "BZh" : 24 : string } -> (* bzip2 *) prerr_endline (s_"diskzip: This looks like a bzip2 archive. Did you mean to pass the '-j' option?"); exit 1 (* If it looks like a disk image (MBR), give an error. *) | { _ : 4080 : bitstring; 0x55 : 8; 0xAA : 8 } -> prerr_endline (s_"diskzip: This looks like a disk image. Did you mean to compress it?"); exit 1 | { _ } -> prerr_endline (s_"diskzip: Not a diskzip archive."); exit 1 *) let () = main ()