(* Unison file synchronizer: src/ubase/util.ml *) (* Copyright 1999-2018, Benjamin C. Pierce 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 3 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, see . *) (*****************************************************************************) (* CASE INSENSITIVE COMPARISON *) (*****************************************************************************) let nocase_cmp a b = let alen = String.length a in let blen = String.length b in let minlen = if alen=minlen then compare alen blen else let c = compare (Char.lowercase(String.get a i)) (Char.lowercase(String.get b i)) in if c<>0 then c else loop (i+1) in loop 0 let nocase_eq a b = (0 = (nocase_cmp a b)) (*****************************************************************************) (* PRE-BUILT MAP AND SET MODULES *) (*****************************************************************************) module StringMap = Map.Make (String) module StringSet = Set.Make (String) let stringSetFromList l = Safelist.fold_right StringSet.add l StringSet.empty (*****************************************************************************) (* Debugging / error messages *) (*****************************************************************************) let infos = ref "" let clear_infos () = if !infos <> "" then begin print_string "\r"; print_string (String.make (String.length !infos) ' '); print_string "\r"; flush stdout end let show_infos () = if !infos <> "" then begin print_string !infos; flush stdout end let set_infos s = if s <> !infos then begin clear_infos (); infos := s; show_infos () end let msg f = clear_infos (); Printf.kfprintf (fun _ -> flush stderr; show_infos ()) stderr f let msg : ('a, out_channel, unit) format -> 'a = msg (* ------------- Formatting stuff --------------- *) let curr_formatter = ref Format.std_formatter let format f = Format.fprintf (!curr_formatter) f let format : ('a, Format.formatter, unit) format -> 'a = format let format_to_string f = let old_formatter = !curr_formatter in curr_formatter := Format.str_formatter; f (); let s = Format.flush_str_formatter () in curr_formatter := old_formatter; s let flush () = Format.pp_print_flush (!curr_formatter) () (*****************************************************************************) (* GLOBAL DEBUGGING SWITCH *) (*****************************************************************************) let debugPrinter = ref None let debug s th = match !debugPrinter with None -> assert false | Some p -> p s th (* This should be set by the UI to a function that can be used to warn users *) let warnPrinter = ref None (* The rest of the program invokes this function to warn users. *) let warn message = match !warnPrinter with None -> () | Some p -> p message (*****************************************************************************) (* EXCEPTION HANDLING *) (*****************************************************************************) exception Fatal of string exception Transient of string let encodeException m kind e = let reraise s = match kind with `Fatal -> raise (Fatal s) | `Transient -> raise (Transient s) in let kindStr = match kind with `Fatal -> "Fatal" | `Transient -> "Transient" in match e with Unix.Unix_error(err,fnname,param) -> let s = "Error in " ^ m ^ ":\n" ^ (Unix.error_message err) ^ " [" ^ fnname ^ "(" ^ param ^ ")]%s" ^ (match err with Unix.EUNKNOWNERR n -> Format.sprintf " (code %d)" n | _ -> "") in debug "exn" (fun() -> msg "Converting a Unix error to %s:\n%s\n" kindStr s); reraise s | Transient(s) -> debug "exn" (fun() -> if kind = `Fatal then msg "In %s: Converting a Transient error to %s:\n%s\n" m kindStr s else msg "In %s: Propagating Transient error\n" m); reraise s | Not_found -> let s = "Not_found raised in " ^ m ^ " (this indicates a bug!)" in debug "exn" (fun() -> msg "Converting a Not_found to %s:\n%s\n" kindStr s); reraise s | Invalid_argument a -> let s = "Invalid_argument("^a^") raised in " ^ m ^ " (this indicates a bug!)" in debug "exn" (fun() -> msg "Converting an Invalid_argument to %s:\n%s\n" kindStr s); reraise s | Sys_error(s) -> let s = "Error in " ^ m ^ ":\n" ^ s in debug "exn" (fun() -> msg "Converting a Sys_error to %s:\n%s\n" kindStr s); reraise s | Sys_blocked_io -> let s = "Blocked IO error in " ^ m in debug "exn" (fun() -> msg "Converting a Sys_blocked_io to %s:\n%s\n" kindStr s); reraise s | _ -> raise e let convertUnixErrorsToExn m f n e = try f() with Unix.Unix_error(err,fnname,param) -> let s = "Error in " ^ m ^ ":\n" ^ (Unix.error_message err) ^ " [" ^ fnname ^ "(" ^ param ^ ")]" in debug "exn" (fun() -> msg "Converting a Unix error to %s:\n%s\n" n s); raise (e s) | Transient(s) -> debug "exn" (fun() -> if n="Fatal" then msg "In %s: Converting a Transient error to %s:\n%s\n" m n s else msg "In %s: Propagating Transient error\n" m); raise (e s) | Not_found -> let s = "Not_found raised in " ^ m ^ " (this indicates a bug!)" in debug "exn" (fun() -> msg "Converting a Not_found to %s:\n%s\n" n s); raise (e s) | End_of_file -> let s = "End_of_file exception raised in " ^ m ^ " (this indicates a bug!)" in debug "exn" (fun() -> msg "Converting an End_of_file to %s:\n%s\n" n s); raise (e s) | Sys_error(s) -> let s = "Error in " ^ m ^ ":\n" ^ s in debug "exn" (fun() -> msg "Converting a Sys_error to %s:\n%s\n" n s); raise (e s) | Sys_blocked_io -> let s = "Blocked IO error in " ^ m in debug "exn" (fun() -> msg "Converting a Sys_blocked_io to %s:\n%s\n" n s); raise (e s) let convertUnixErrorsToFatal m f = convertUnixErrorsToExn m f "Fatal" (fun str -> Fatal(str)) let convertUnixErrorsToTransient m f = convertUnixErrorsToExn m f "Transient" (fun str -> Transient(str)) let unwindProtect f cleanup = try f () with Transient _ as e -> debug "exn" (fun () -> msg "Exception caught by unwindProtect\n"); convertUnixErrorsToFatal "unwindProtect" (fun()-> cleanup e); raise e let finalize f cleanup = try let res = f () in cleanup (); res with Transient _ as e -> debug "exn" (fun () -> msg "Exception caught by finalize\n"); convertUnixErrorsToFatal "finalize" cleanup; raise e type confirmation = Succeeded | Failed of string let ignoreTransientErrors thunk = try thunk() with Transient(s) -> () let printException e = try raise e with Transient s -> s | Fatal s -> s | e -> Printexc.to_string e (* Safe version of Unix getenv -- raises a comprehensible error message if called with an env variable that doesn't exist *) let safeGetenv var = convertUnixErrorsToFatal "querying environment" (fun () -> try System.getenv var with Not_found -> raise (Fatal ("Environment variable " ^ var ^ " not found"))) let process_status_to_string = function Unix.WEXITED i -> Printf.sprintf "Exited with status %d" i | Unix.WSIGNALED i -> Printf.sprintf "Killed by signal %d" i | Unix.WSTOPPED i -> Printf.sprintf "Stopped by signal %d" i (*****************************************************************************) (* OS TYPE *) (*****************************************************************************) let osType = match Sys.os_type with "Win32" | "Cygwin" -> `Win32 | "Unix" -> `Unix | other -> raise (Fatal ("Unknown OS: " ^ other)) let isCygwin = (Sys.os_type = "Cygwin") (*****************************************************************************) (* MISCELLANEOUS *) (*****************************************************************************) let monthname n = Safelist.nth ["Jan";"Feb";"Mar";"Apr";"May";"Jun";"Jul";"Aug";"Sep";"Oct";"Nov";"Dec"] n let localtime f = convertUnixErrorsToTransient "localtime" (fun()-> Unix.localtime f) let time () = convertUnixErrorsToTransient "time" Unix.time let time2string timef = try let time = localtime timef in (* Old-style: Printf.sprintf "%2d:%.2d:%.2d on %2d %3s, %4d" time.Unix.tm_hour time.Unix.tm_min time.Unix.tm_sec time.Unix.tm_mday (monthname time.Unix.tm_mon) (time.Unix.tm_year + 1900) *) Printf.sprintf "%4d-%02d-%02d at %2d:%.2d:%.2d" (time.Unix.tm_year + 1900) (time.Unix.tm_mon + 1) time.Unix.tm_mday time.Unix.tm_hour time.Unix.tm_min time.Unix.tm_sec with Transient _ -> "(invalid date)" let percentageOfTotal current total = (int_of_float ((float current) *. 100.0 /. (float total))) let percent2string p = Printf.sprintf "%3d%%" (truncate (max 0. (min 100. p))) let extractValueFromOption = function None -> raise (Fatal "extractValueFromOption failed") | Some(v) -> v let option2string (prt: 'a -> string) = function Some x -> prt x | None -> "N.A." (*****************************************************************************) (* String utility functions *) (*****************************************************************************) let truncateString string length = let actualLength = String.length string in if actualLength <= length then string^(String.make (length - actualLength) ' ') else if actualLength < 3 then string else (String.sub string 0 (length - 3))^ "..." let findsubstring s1 s2 = let l1 = String.length s1 in let l2 = String.length s2 in let rec loop i = if i+l1 > l2 then None else if s1 = String.sub s2 i l1 then Some(i) else loop (i+1) in loop 0 let rec replacesubstring s fromstring tostring = match findsubstring fromstring s with None -> s | Some(i) -> let before = String.sub s 0 i in let afterpos = i + (String.length fromstring) in let after = String.sub s afterpos ((String.length s) - afterpos) in before ^ tostring ^ (replacesubstring after fromstring tostring) let replacesubstrings s pairs = Safelist.fold_left (fun s' (froms,tos) -> replacesubstring s' froms tos) s pairs let startswith s1 s2 = let l1 = String.length s1 in let l2 = String.length s2 in if l1 < l2 then false else let rec loop i = if i>=l2 then true else if s1.[i] <> s2.[i] then false else loop (i+1) in loop 0 let endswith s1 s2 = let l1 = String.length s1 in let l2 = String.length s2 in let offset = l1 - l2 in if l1 < l2 then false else let rec loop i = if i>=l2 then true else if s1.[i+offset] <> s2.[i] then false else loop (i+1) in loop 0 let concatmap sep f l = String.concat sep (Safelist.map f l) let removeTrailingCR s = let l = String.length s in if l = 0 || s.[l - 1] <> '\r' then s else String.sub s 0 (l - 1) let trimWhitespace s = let l = String.length s in let rec loop lp rp = if lp > rp then "" else if s.[lp]=' ' || s.[lp]='\t' || s.[lp]='\n' || s.[lp]='\r' then loop (lp+1) rp else if s.[rp]=' ' || s.[rp]='\t' || s.[rp]='\n' || s.[rp]='\r' then loop lp (rp-1) else String.sub s lp (rp+1-lp) in loop 0 (l-1) let splitAtFirstChar (s:string) (c:char) = try let i = String.index s c and l= String.length s in (* rest is possibly the empty string *) [String.sub s 0 i; String.sub s (i+1) (l-i-1)] with Not_found -> [s] let splitIntoWords ?esc:(e='\\') (s:string) (c:char) = let rec inword acc eacc start pos = if pos >= String.length s || s.[pos] = c then let word = String.concat "" (Safelist.rev (String.sub s start (pos-start)::eacc)) in betweenwords (word::acc) pos else if s.[pos] = e then inescape acc eacc start pos else inword acc eacc start (pos+1) and inescape acc eacc start pos = let eword = String.sub s start (pos-start) in if pos+1 >= String.length s then inword acc (eword::eacc) (pos+1) (pos+1) (* ignore final esc *) else (* take any following char *) let echar = String.make 1 (String.get s (pos+1)) in inword acc (echar::eword::eacc) (pos+2) (pos+2) and betweenwords acc pos = if pos >= String.length s then (Safelist.rev acc) else if s.[pos]=c then betweenwords acc (pos+1) else inword acc [] pos pos in betweenwords [] 0 let rec splitIntoWordsByString s sep = match findsubstring sep s with None -> [s] | Some(i) -> let before = String.sub s 0 i in let afterpos = i + (String.length sep) in let after = String.sub s afterpos ((String.length s) - afterpos) in before :: (splitIntoWordsByString after sep) let padto n s = s ^ (String.make (max 0 (n - String.length s)) ' ') (*****************************************************************************) (* Building pathnames in the user's home dir *) (*****************************************************************************) let homeDirStr = (if (osType = `Unix) || isCygwin then safeGetenv "HOME" else if osType = `Win32 then (*We don't want the behavior of Unison to depends on whether it is run from a Cygwin shell (where HOME is set) or in any other way (where HOME is usually not set) try System.getenv "HOME" (* Windows 9x with Cygwin HOME set *) with Not_found -> *) try System.getenv "USERPROFILE" (* Windows NT/2K standard *) with Not_found -> try System.getenv "UNISON" (* Use UNISON dir if it is set *) with Not_found -> "c:/" (* Default *) else assert false (* osType can't be anything else *)) let homeDir () = System.fspathFromString homeDirStr let fileInHomeDir n = System.fspathConcat (homeDir ()) n let fileMaybeRelToHomeDir n = if Filename.is_relative n then fileInHomeDir n else System.fspathFromString n (*****************************************************************************) (* "Upcall" for building pathnames in the .unison dir *) (*****************************************************************************) let fileInUnisonDirFn = ref None let supplyFileInUnisonDirFn f = fileInUnisonDirFn := Some(f) let fileInUnisonDir n = match !fileInUnisonDirFn with None -> assert false | Some(f) -> f n