(* Modified from the ML Kit 4.1.4; basislib/Path.sml * by mfluet@acm.org on 2005-8-10 based on * modifications from the ML Kit 3 Version; basislib/Path.sml * by sweeks@research.nj.nec.com on 1999-1-5. *) structure OS_Path: OS_PATH = struct exception Path exception InvalidArc (* It would make sense to use substrings for internal versions of * fromString and toString, and to allocate new strings only when * externalizing the strings. * Impossible cases: UNIX: {isAbs = false, vol = _, arcs = "" :: _} Mac: {isAbs = true, vol = _, arcs = "" :: _} *) val op @ = List.@ infix 9 sub val op sub = String.sub val substring = String.extract val isWindows = let open Primitive.MLton.Platform.OS in host = MinGW end (* the path separator used in canonical paths *) val slash = if isWindows then "\\" else "/" (* MinGW and newer Windows commands treat both / and \ as path * separators. * * Sadly this means that toString o fromString is not the identity * b/c foo/bar -> foo\bar. However, there's nothing else one can do! * This diverges from the standard. *) fun isslash c = c = #"/" orelse (isWindows andalso c = #"\\") fun iscolon c = c = #":" fun isVolumeName v = (isWindows andalso size v = 2 andalso Char.isAlpha (v sub 0) andalso iscolon (v sub 1)) fun volumeMatch (root, relative) = relative = "" orelse (isVolumeName root andalso isVolumeName relative andalso (Char.toUpper (root sub 0) = Char.toUpper (relative sub 0))) fun canonName a = if isWindows then String.translate (str o Char.toLower) a else a val parentArc = ".." val currentArc = "." (* Ahh joy. The SML basis library standard and Windows paths. * * The big problem with windows paths is "\foo"" * - It's not absolute, since chdir("A:\") may switch from "C:", thus * changing the meaning of "\foo". *) fun validVolume {isAbs, vol} = if isWindows then isVolumeName vol orelse (not isAbs andalso vol = "") else vol = "" fun fromString s = let val (vol, rest) = (* 4:foo has a volume of "4:" even tho invalid *) if isWindows andalso size s >= 2 andalso iscolon (s sub 1) then (substring (s, 0, SOME 2), substring (s, 2, NONE)) else ("", s) val (isAbs, arcs) = case (String.fields isslash rest) of "" :: [] => (false, []) | "" :: r => (true, r) | r => (false, r) in {arcs = arcs, isAbs = isAbs, vol = vol} end val getVolume = #vol o fromString val isAbsolute = #isAbs o fromString val isRelative = not o isAbsolute fun isArc s = s = "" orelse (case fromString s of {arcs = [_], isAbs = false, vol = ""} => true | _ => false) fun toString {arcs, isAbs, vol} = if not (validVolume {isAbs = isAbs, vol = vol}) then raise Path else if not isAbs andalso case arcs of ("" :: _) => true | _ => false then raise Path else if List.exists (not o isArc) arcs then raise InvalidArc else concat [vol, if isAbs then slash else "", String.concatWith slash arcs] fun concatArcs (a1, a2) = let val a1 = case List.rev a1 of "" :: r => List.rev r | _ => a1 in a1 @ a2 end fun concat (p1, p2) = let val {arcs = a1, isAbs, vol = v1} = fromString p1 val {arcs = a2, isAbs = isAbs2, vol = v2} = fromString p2 in if isAbs2 orelse not (volumeMatch (v1, v2)) then raise Path else toString {arcs = concatArcs (a1, a2), isAbs = isAbs, vol = v1} end fun getParent p = let val {isAbs, vol, arcs} = fromString p val arcs = List.rev (case List.rev arcs of [] => [parentArc] | "." :: r => parentArc :: r | ".." :: r => parentArc :: parentArc :: r | _ :: [] => if isAbs then [""] else [currentArc] | "" :: r => parentArc :: r | _ :: r => r) in toString {arcs = arcs, isAbs = isAbs, vol = vol} end fun mkCanonical p = let val {arcs, isAbs, vol} = fromString p fun backup l = case l of [] => if isAbs then [] else [parentArc] | first :: res => if first = ".." then parentArc :: parentArc :: res else res fun reduce arcs = let fun h (l, res) = case l of [] => (case res of [] => if isAbs then [""] else [currentArc] | _ => res ) | a1 :: ar => if a1 = "" orelse a1 = "." then h (ar, res) else if a1 = ".." then h (ar, backup res) else h (ar, canonName a1 :: res) in h (arcs, []) end in toString {arcs = List.rev (reduce arcs), isAbs = isAbs, vol = canonName vol} end val rec parentize = fn [] => [] | _ :: ar => parentArc :: parentize ar fun mkRelative {path = p1, relativeTo = p2} = let val {arcs = arcs1, isAbs = isAbs1, vol = vol1} = fromString p1 val {arcs = arcs2, isAbs = isAbs2, vol = vol2} = fromString (mkCanonical p2) in if not isAbs2 then raise Path else if not isAbs1 then p1 else let fun h (a1, a2) = case (a1, a2) of ([], []) => ["."] | (_, []) => a1 | ([], a2) => parentize a2 | (a11 :: a1r, a21 :: a2r) => if canonName a11 = a21 then h (a1r, a2r) else parentize a2 @ (if arcs1 = [""] then [] else a1) in if not (volumeMatch (vol2, vol1)) then raise Path else toString {arcs = h (arcs1, arcs2), isAbs = false, vol = ""} end end fun mkAbsolute {path = p1, relativeTo = p2} = if isRelative p2 then raise Path else if isAbsolute p1 then p1 else mkCanonical (concat (p2, p1)) fun isCanonical p = mkCanonical p = p fun joinDirFile {dir, file} = let val {arcs, isAbs, vol} = fromString dir val arcs = case (arcs, file) of ([], "") => [] | _ => concatArcs (arcs, [file]) in toString {arcs = arcs, isAbs = isAbs, vol = vol} end fun splitDirFile p = let open List val {isAbs, vol, arcs} = fromString p in case rev arcs of [] => {dir = p, file = ""} | arcn :: farcs => {dir = toString {arcs = rev farcs, isAbs = isAbs, vol = vol}, file = arcn} end val dir = #dir o splitDirFile val file = #file o splitDirFile fun joinBaseExt {base, ext} = case ext of NONE => base | SOME ex => if ex = "" then base else String.concat [base, ".", ex] fun splitBaseExt s = let val {dir, file} = splitDirFile s open Substring val (fst, snd) = splitr (fn c => c <> #".") (full file) in if isEmpty snd (* dot at right end *) orelse isEmpty fst (* no dot *) orelse size fst = 1 (* dot at left end only *) then {base = s, ext = NONE} else {base = joinDirFile {dir = dir, file = string (trimr 1 fst)}, ext = SOME (string snd)} end val ext = #ext o splitBaseExt val base = #base o splitBaseExt fun isRoot path = case fromString path of {isAbs = true, arcs=[""], ...} => true | _ => false fun fromUnixPath s = if not isWindows then s else if Char.contains s (slash sub 0) then raise InvalidArc else String.translate (fn c => if c = #"/" then slash else str c) s fun toUnixPath s = if not isWindows then s else let val {arcs, isAbs, vol} = fromString s in if vol <> "" then raise Path else (if isAbs then "/" else "") ^ String.concatWith "/" arcs end end