Skip to content

Commit

Permalink
Preserve quoting in environment variables
Browse files Browse the repository at this point in the history
Change the internal representation of environment variables so that both
the original quoted version and the parsed version are kept, along with
the separator. This has the benefit that reconstituting the variable
value does not require knowledge of the separator (which deals
coherently with the parasitic case of two packages updating the same
variable with a different separator: the value is almost certainly
trashed, but it is at least trashed in a semantically coherent manner!)

Co-authored-by: Raja Boujbel <raja.boujbel@ocamlpro.com>
  • Loading branch information
dra27 and rjbou committed May 15, 2024
1 parent 4eb951d commit 5db5951
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 183 deletions.
217 changes: 150 additions & 67 deletions src/state/opamEnv.ml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,20 @@ type sep_path_format = [
| `rewrite of separator * path_format (* path, rewrite using sep & fmt *)
]

let transform_format ~(sepfmt:sep_path_format) =
type transform = {
tr_entry: string; (* Entry (directory) in native, normalised form *)
tr_raw: string; (* Actual string to put into the final variable *)
tr_sep: char; (* Separator to use if (and only if) any entries follow *)
}

let transform_format ~(sepfmt:sep_path_format) var =
match sepfmt with
| `norewrite -> fun x -> x
| `norewrite ->
fun arg ->
{ tr_entry = arg;
tr_raw = arg;
tr_sep = OpamTypesBase.char_of_separator (fst (default_sep_fmt var));
}
| (`rewrite_default _ | `rewrite _) as sepfmt ->
let separator, format =
match sepfmt with
Expand All @@ -64,14 +75,26 @@ let transform_format ~(sepfmt:sep_path_format) =
(* noop on non windows *)
(Lazy.force OpamSystem.get_cygpath_path_transform) ~pathlist:false
in
let separator = OpamTypesBase.char_of_separator separator in
match format with
| Target | Host -> translate
| Target | Host ->
fun arg ->
let path = translate arg in
{ tr_entry = path;
tr_raw = path;
tr_sep = separator;
}
| Target_quoted | Host_quoted ->
fun arg ->
let path = translate arg in
let separator = OpamTypesBase.char_of_separator separator in
if String.contains path separator then
"\""^path^"\"" else path
let quoted_path =
if String.contains path separator then
"\""^path^"\"" else path
in
{ tr_entry = path;
tr_raw = quoted_path;
tr_sep = separator;
}

let resolve_separator_and_format :
type r. (r, 'a) env_update -> (spf_resolved, 'a) env_update =
Expand Down Expand Up @@ -134,38 +157,91 @@ let resolve_separator_and_format :
in
{ upd with envu_rewrite }

let split_path_variable path sep =
let length = String.length path in
let rec f acc index current current_raw last normal =
if (index : int) = length then
let final = String.sub path last (index - last) in
let current = current ^ final in
let current_raw = current_raw ^ final in
let elem = {tr_entry = current; tr_raw = current_raw; tr_sep = sep } in
List.rev (elem::acc)
else
let c = path.[index]
and next = succ index in
if c = sep && normal || c = '"' then
let segment = String.sub path last (index - last) in
let current = current ^ segment in
let current_raw = current_raw ^ segment in
let elem = {tr_entry = current; tr_raw = current_raw; tr_sep = sep } in
if c = '"' then
f acc next current (current_raw ^ "\"") next (not normal)
else if (next : int) = length then (* path ends with a separator *)
let empty = { tr_entry = ""; tr_raw = ""; tr_sep = sep } in
List.rev (empty::elem::acc)
else (* c = sep; text follows *)
f (elem::acc) next "" "" next true
else
f acc next current current_raw last normal
in
f [] 0 "" "" 0 true

(* - Environment and updates handling - *)
let split_var ~(sepfmt:sep_path_format) var value =
match sepfmt with
| `norewrite ->
default_sep_fmt var
|> fst
|> char_of_separator
|> OpamStd.String.split value
let sep = char_of_separator (fst (default_sep_fmt var)) in
List.map (fun s ->
{ tr_entry = s; tr_raw = s; tr_sep = sep})
(OpamStd.String.split_delim value sep)
| (`rewrite_default _ | `rewrite _) as sepfmt ->
let separator, format =
match sepfmt with
| `rewrite_default var -> default_sep_fmt_str var
| `rewrite (sep, fmt) -> sep, fmt
in
let sep = OpamTypesBase.char_of_separator separator in
if (value : string) = String.make 1 sep then
[{ tr_entry = ""; tr_raw = value; tr_sep = sep }]
else
match format with
| Target | Host ->
OpamStd.String.split value sep
List.map (fun s ->
{ tr_entry = s; tr_raw = s; tr_sep = sep})
(OpamStd.String.split_delim value sep)
| Target_quoted | Host_quoted ->
OpamStd.String.split_quoted value sep

let join_var ~(sepfmt:sep_path_format) var values =
let separator =
split_path_variable value sep

(* Auxiliaries for join_var - cf. String.concat *)
let rec sum_lengths acc = function
| [{ tr_raw = raw; _}] -> acc + String.length raw
| { tr_raw = raw; _}::tl -> sum_lengths (acc + String.length raw + 1) tl
| [] -> acc (* semantically unreachable *)

let rec unsafe_blits dst pos = function
| [] ->
Bytes.unsafe_to_string dst
| [{ tr_raw = raw; _}] ->
String.unsafe_blit raw 0 dst pos (String.length raw);
Bytes.unsafe_to_string dst
| { tr_raw = raw; tr_sep = sep; _}::tl ->
let length = String.length raw in
String.unsafe_blit raw 0 dst pos length;
Bytes.unsafe_set dst (pos + length) sep;
unsafe_blits dst (pos + length + 1) tl

let join_var values =
if values = [] then "" else
unsafe_blits (Bytes.create (sum_lengths 0 values)) 0 values

let separator_char_for ~sepfmt var =
let (separator, _) =
match sepfmt with
| `norewrite -> fst (default_sep_fmt var)
| `rewrite_default var -> fst (default_sep_fmt_str var)
| `rewrite (sep, _) -> sep
| `norewrite -> default_sep_fmt var
| `rewrite_default var -> default_sep_fmt_str var
| `rewrite spf -> spf
in
String.concat
(String.make 1 (OpamTypesBase.char_of_separator separator))
values

OpamTypesBase.char_of_separator separator

(* To allow in-place updates, we store intermediate values of path-like as a
pair of list [(rl1, l2)] such that the value is [List.rev_append rl1 l2] and
Expand All @@ -176,74 +252,88 @@ let unzip_to ~sepfmt var elt current =
(* If [r = l @ rs] then [remove_prefix l r] is [Some rs], otherwise [None] *)
let rec remove_prefix l r =
match l, r with
| (l::ls, r::rs) when l = r ->
| {tr_entry = l; _}::ls, { tr_entry = r; _}::rs when l = r ->
remove_prefix ls rs
| ([], rs) -> Some rs
| _ -> None
in
match (if String.equal elt "" then [""]
else split_var ~sepfmt var elt) with
(* Split elt if necessary *)
let elts =
if String.equal elt "" then
[{ tr_entry = ""; tr_raw = "";
tr_sep = separator_char_for ~sepfmt var }]
else split_var ~sepfmt var elt
in
match elts with
| [] -> invalid_arg "OpamEnv.unzip_to"
| hd::tl ->
| { tr_entry = hd; _}::tl ->
let rec aux acc = function
| [] -> None
| x::r ->
| ({ tr_entry = x; _} as v)::r ->
if String.equal x hd then
match remove_prefix tl r with
| Some r -> Some (acc, r)
| None -> aux (x::acc) r
else aux (x::acc) r
| None -> aux (v::acc) r
else aux (v::acc) r
in
aux [] current

let rezip ?insert (l1, l2) =
List.rev_append l1 (match insert with None -> l2 | Some i -> i::l2)

let rezip_to_string ~sepfmt var ?insert z =
join_var ~sepfmt var (rezip ?insert z)

let rezip_to_string ?insert z =
join_var (rezip ?insert z)

let apply_op_zip ~sepfmt op arg (rl1,l2 as zip) =
let arg = transform_format ~sepfmt arg in
let apply_op_zip ~sepfmt var op arg (rl1,l2 as zip) =
let arg = transform_format ~sepfmt var arg in
let empty_tr = { tr_entry = ""; tr_raw = ""; tr_sep = arg.tr_sep } in
let colon_eq ?(eqcol=false) = function (* prepend a, but keep ":"s *)
| [] | [""] -> [], [arg; ""]
| "" :: l ->
| [] | [{ tr_entry = ""; _}] -> [], [arg; empty_tr]
| { tr_entry = ""; _} :: l ->
(* keep surrounding colons *)
if eqcol then l@[""], [arg] else l, [""; arg]
if eqcol then l@[empty_tr], [arg] else l, [empty_tr; arg]
| l -> l, [arg]
in
let cygwin path =
let contains_in dir item =
let contains_in {tr_entry = dir; _} item =
Sys.file_exists (Filename.concat dir item)
in
let shadow_list =
List.filter (contains_in arg)
["bash.exe"; "sort.exe"; "tar.exe"; "git.exe"]
["bash.exe"; "sort.exe"; "tar.exe"; "git.exe"]
in
let rec loop acc = function
let rec loop acc = function
| [] -> acc, [arg]
| (d::rest) as suffix ->
if List.exists (contains_in d) shadow_list then
acc, arg::suffix
else
loop (d::acc) rest
in
loop [] path
if List.exists (contains_in d) shadow_list then
acc, arg::suffix
else
loop (d::acc) rest
in
loop [] path
in
match op with
| Eq -> [],[arg]
| PlusEq ->
(* New value goes at head of existing list; no prefix *)
begin match rezip zip with
| [""] -> [], [arg]
| [{ tr_entry = ""; tr_raw = raw; _}] ->
if raw = "" then
[], [arg]
else
[], [arg; empty_tr]
| zip -> [], arg::zip
end
| EqPlus ->
(* NB List.rev_append l2 rl1 is equivalent to
List.rev (List.rev_append rl1 l2)
Place new value at the end *)
begin match List.rev_append l2 rl1 with
| [""] -> [], [arg]
| [{ tr_entry = ""; tr_raw = raw; _}] ->
if raw = "" then
[], [arg]
else
[], [empty_tr; arg]
| zip -> zip, [arg]
end
| Cygwin ->
Expand All @@ -267,11 +357,11 @@ let apply_op_zip ~sepfmt op arg (rl1,l2 as zip) =
or empty lists is returned if the variable should be unset or has an unknown
previous value. *)
let reverse_env_update ~sepfmt var op arg cur_value =
let arg = transform_format ~sepfmt arg in
if String.equal arg "" && op <> Eq then None else
let { tr_entry = arg; _} = transform_format ~sepfmt var arg in
if String.equal arg "" && op <> Eq then None else
match op with
| Eq ->
if arg = join_var ~sepfmt var cur_value
if arg = join_var cur_value
then Some ([],[]) else None
| PlusEq | EqPlusEq -> unzip_to var ~sepfmt arg cur_value
| EqPlus | Cygwin ->
Expand All @@ -280,11 +370,11 @@ let reverse_env_update ~sepfmt var op arg cur_value =
| Some (rl1, l2) -> Some (List.rev l2, List.rev rl1))
| ColonEq ->
(match unzip_to var ~sepfmt arg cur_value with
| Some ([], [""]) -> Some ([], [])
| Some ([], [{ tr_entry = ""; _}]) -> Some ([], [])
| r -> r)
| EqColon ->
(match unzip_to ~sepfmt var arg (List.rev cur_value) with
| Some ([], [""]) -> Some ([], [])
| Some ([], [{ tr_entry = ""; _}]) -> Some ([], [])
| Some (rl1, l2) -> Some (List.rev l2, List.rev rl1)
| None -> None)

Expand Down Expand Up @@ -423,7 +513,7 @@ let expand updates =
in
let acc =
if String.equal arg "" && op <> Eq then acc else
((var, apply_op_zip ~sepfmt op arg zip, doc, sepfmt)
((var, apply_op_zip ~sepfmt var op arg zip, doc, sepfmt)
:: acc)
in
apply_updates
Expand All @@ -433,10 +523,10 @@ let expand updates =
| [] ->
List.rev
@@ List.rev_append
(List.rev_map (fun (var, z, doc, sepfmt) ->
var, rezip_to_string ~sepfmt var z, doc) acc)
@@ List.rev_map (fun (var, z, sepfmt) ->
var, rezip_to_string ~sepfmt var z,
(List.rev_map (fun (var, z, doc, _sepfmt) ->
var, rezip_to_string z, doc) acc)
@@ List.rev_map (fun (var, z, _sepfmt) ->
var, rezip_to_string z,
Some "Reverting previous opam update")
reverts
in
Expand Down Expand Up @@ -957,15 +1047,8 @@ let string_of_update st shell updates =
| Some (SPF_Resolved None) -> `rewrite_default envu_var
| Some (SPF_Resolved (Some spf)) -> `rewrite spf
in
let string =
transform_format ~sepfmt string
in
let sep =
OpamTypesBase.char_of_separator
(match envu_rewrite with
| Some (SPF_Resolved (Some (sep, _))) -> sep
| None | Some (SPF_Resolved None) ->
fst @@ default_sep_fmt_str envu_var)
let { tr_raw = string; tr_sep = sep; _} =
transform_format ~sepfmt (OpamStd.Env.Name.of_string envu_var) string
in
let key, value =
envu_var, match (envu_op : euok_writeable env_update_op_kind) with
Expand Down
Loading

0 comments on commit 5db5951

Please sign in to comment.