| author | haftmann | 
| Fri, 18 Oct 2019 18:41:33 +0000 | |
| changeset 70901 | 94a0c47b8553 | 
| parent 69904 | 6f5bd59f75f4 | 
| child 71383 | 8313dca6dee9 | 
| permissions | -rw-r--r-- | 
| 43600 | 1  | 
/* Title: Pure/General/path.scala  | 
2  | 
Author: Makarius  | 
|
3  | 
||
| 43601 | 4  | 
Algebra of file-system paths: basic POSIX notation, extended by named  | 
5  | 
roots (e.g. //foo) and variables (e.g. $BAR).  | 
|
| 43600 | 6  | 
*/  | 
7  | 
||
8  | 
package isabelle  | 
|
9  | 
||
10  | 
||
| 48409 | 11  | 
import java.io.{File => JFile}
 | 
| 48373 | 12  | 
|
| 
43697
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
13  | 
import scala.util.matching.Regex  | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
14  | 
|
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
15  | 
|
| 43600 | 16  | 
object Path  | 
17  | 
{
 | 
|
18  | 
/* path elements */  | 
|
19  | 
||
| 45244 | 20  | 
sealed abstract class Elem  | 
| 60215 | 21  | 
private case class Root(name: String) extends Elem  | 
22  | 
private case class Basic(name: String) extends Elem  | 
|
23  | 
private case class Variable(name: String) extends Elem  | 
|
| 43600 | 24  | 
private case object Parent extends Elem  | 
25  | 
||
26  | 
private def err_elem(msg: String, s: String): Nothing =  | 
|
| 69547 | 27  | 
error(msg + " path element " + quote(s))  | 
28  | 
||
| 
69548
 
415dc92050a6
more strict check: avoid confusion of Path.basic with Path.current / Path.parent;
 
wenzelm 
parents: 
69547 
diff
changeset
 | 
29  | 
  private val illegal_elem = Set("", "~", "~~", ".", "..")
 | 
| 
69550
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
30  | 
private val illegal_char = "/\\$:\"'<>|?*"  | 
| 43600 | 31  | 
|
32  | 
private def check_elem(s: String): String =  | 
|
| 69547 | 33  | 
    if (illegal_elem.contains(s)) err_elem("Illegal", s)
 | 
| 52106 | 34  | 
    else {
 | 
| 
69550
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
35  | 
      for (c <- s) {
 | 
| 
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
36  | 
if (c.toInt < 32)  | 
| 
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
37  | 
          err_elem("Illegal control character " + c.toInt + " in", s)
 | 
| 
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
38  | 
if (illegal_char.contains(c))  | 
| 
 
57ff523d9008
reject further illegal chars according to https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 
wenzelm 
parents: 
69549 
diff
changeset
 | 
39  | 
          err_elem("Illegal character " + quote(c.toString) + " in", s)
 | 
| 69547 | 40  | 
}  | 
| 52106 | 41  | 
s  | 
42  | 
}  | 
|
| 43600 | 43  | 
|
44  | 
private def root_elem(s: String): Elem = Root(check_elem(s))  | 
|
45  | 
private def basic_elem(s: String): Elem = Basic(check_elem(s))  | 
|
46  | 
private def variable_elem(s: String): Elem = Variable(check_elem(s))  | 
|
47  | 
||
48  | 
private def apply_elem(y: Elem, xs: List[Elem]): List[Elem] =  | 
|
49  | 
    (y, xs) match {
 | 
|
50  | 
case (Root(_), _) => List(y)  | 
|
51  | 
case (Parent, Root(_) :: _) => xs  | 
|
52  | 
case (Parent, Basic(_) :: rest) => rest  | 
|
53  | 
case _ => y :: xs  | 
|
54  | 
}  | 
|
55  | 
||
56  | 
private def norm_elems(elems: List[Elem]): List[Elem] =  | 
|
57  | 
(elems :\ (Nil: List[Elem]))(apply_elem)  | 
|
58  | 
||
| 56844 | 59  | 
private def implode_elem(elem: Elem, short: Boolean): String =  | 
| 43600 | 60  | 
    elem match {
 | 
61  | 
      case Root("") => ""
 | 
|
62  | 
case Root(s) => "//" + s  | 
|
63  | 
case Basic(s) => s  | 
|
| 56844 | 64  | 
      case Variable("USER_HOME") if short => "~"
 | 
65  | 
      case Variable("ISABELLE_HOME") if short => "~~"
 | 
|
| 43600 | 66  | 
case Variable(s) => "$" + s  | 
67  | 
case Parent => ".."  | 
|
68  | 
}  | 
|
69  | 
||
70  | 
||
71  | 
/* path constructors */  | 
|
72  | 
||
| 45244 | 73  | 
val current: Path = new Path(Nil)  | 
74  | 
  val root: Path = new Path(List(Root("")))
 | 
|
75  | 
def named_root(s: String): Path = new Path(List(root_elem(s)))  | 
|
| 69670 | 76  | 
def make(elems: List[String]): Path = new Path(elems.reverse.map(basic_elem(_)))  | 
| 45244 | 77  | 
def basic(s: String): Path = new Path(List(basic_elem(s)))  | 
78  | 
def variable(s: String): Path = new Path(List(variable_elem(s)))  | 
|
79  | 
val parent: Path = new Path(List(Parent))  | 
|
| 43600 | 80  | 
|
81  | 
||
82  | 
/* explode */  | 
|
83  | 
||
84  | 
def explode(str: String): Path =  | 
|
85  | 
  {
 | 
|
| 52106 | 86  | 
def explode_elem(s: String): Elem =  | 
87  | 
      try {
 | 
|
88  | 
if (s == "..") Parent  | 
|
89  | 
        else if (s == "~") Variable("USER_HOME")
 | 
|
90  | 
        else if (s == "~~") Variable("ISABELLE_HOME")
 | 
|
91  | 
        else if (s.startsWith("$")) variable_elem(s.substring(1))
 | 
|
92  | 
else basic_elem(s)  | 
|
93  | 
}  | 
|
94  | 
      catch { case ERROR(msg) => cat_error(msg, "The error(s) above occurred in " + quote(str)) }
 | 
|
95  | 
||
| 
43670
 
7f933761764b
prefer space_explode/split_lines as in Isabelle/ML;
 
wenzelm 
parents: 
43669 
diff
changeset
 | 
96  | 
    val ss = space_explode('/', str)
 | 
| 43600 | 97  | 
val r = ss.takeWhile(_.isEmpty).length  | 
98  | 
val es = ss.dropWhile(_.isEmpty)  | 
|
99  | 
val (roots, raw_elems) =  | 
|
100  | 
if (r == 0) (Nil, es)  | 
|
101  | 
      else if (r == 1) (List(Root("")), es)
 | 
|
102  | 
      else if (es.isEmpty) (List(Root("")), Nil)
 | 
|
103  | 
else (List(root_elem(es.head)), es.tail)  | 
|
| 52106 | 104  | 
val elems = raw_elems.filterNot(s => s.isEmpty || s == ".").map(explode_elem)  | 
105  | 
||
| 63866 | 106  | 
new Path(norm_elems(elems reverse_::: roots))  | 
| 43600 | 107  | 
}  | 
| 43669 | 108  | 
|
| 
55879
 
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
 
wenzelm 
parents: 
55555 
diff
changeset
 | 
109  | 
def is_wellformed(str: String): Boolean =  | 
| 48484 | 110  | 
    try { explode(str); true } catch { case ERROR(_) => false }
 | 
111  | 
||
| 
55879
 
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
 
wenzelm 
parents: 
55555 
diff
changeset
 | 
112  | 
def is_valid(str: String): Boolean =  | 
| 
 
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
 
wenzelm 
parents: 
55555 
diff
changeset
 | 
113  | 
    try { explode(str).expand; true } catch { case ERROR(_) => false }
 | 
| 
 
ac979f750c1a
clarified path checks: avoid crash of rendering due to spurious errors;
 
wenzelm 
parents: 
55555 
diff
changeset
 | 
114  | 
|
| 43669 | 115  | 
def split(str: String): List[Path] =  | 
| 
43670
 
7f933761764b
prefer space_explode/split_lines as in Isabelle/ML;
 
wenzelm 
parents: 
43669 
diff
changeset
 | 
116  | 
    space_explode(':', str).filterNot(_.isEmpty).map(explode)
 | 
| 48457 | 117  | 
|
118  | 
||
119  | 
/* encode */  | 
|
120  | 
||
121  | 
val encode: XML.Encode.T[Path] = (path => XML.Encode.string(path.implode))  | 
|
| 
69551
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
122  | 
|
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
123  | 
|
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
124  | 
/* reserved names */  | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
125  | 
|
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
126  | 
private val reserved_windows: Set[String] =  | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
127  | 
    Set("CON", "PRN", "AUX", "NUL",
 | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
128  | 
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",  | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
129  | 
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9")  | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
130  | 
|
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
131  | 
def is_reserved(name: String): Boolean =  | 
| 
 
adb52af5ba55
exclude file name components that are special on Windows;
 
wenzelm 
parents: 
69550 
diff
changeset
 | 
132  | 
Long_Name.explode(name).exists(a => reserved_windows.contains(Word.uppercase(a)))  | 
| 69904 | 133  | 
|
134  | 
||
135  | 
/* case-insensitive names */  | 
|
136  | 
||
137  | 
def check_case_insensitive(paths: List[Path])  | 
|
138  | 
  {
 | 
|
139  | 
val table =  | 
|
140  | 
      (Multi_Map.empty[String, String] /: paths)({ case (tab, path) =>
 | 
|
141  | 
val name = path.expand.implode  | 
|
142  | 
tab.insert(Word.lowercase(name), name)  | 
|
143  | 
})  | 
|
144  | 
val collisions =  | 
|
145  | 
      (for { (_, coll) <- table.iterator_list if coll.length > 1 } yield coll).toList.flatten
 | 
|
146  | 
    if (collisions.nonEmpty) {
 | 
|
147  | 
      error(("Collision of file names due case-insensitivity:" :: collisions).mkString("\n  "))
 | 
|
148  | 
}  | 
|
149  | 
}  | 
|
| 43600 | 150  | 
}  | 
151  | 
||
| 43669 | 152  | 
|
| 46712 | 153  | 
final class Path private(private val elems: List[Path.Elem]) // reversed elements  | 
| 43600 | 154  | 
{
 | 
155  | 
def is_current: Boolean = elems.isEmpty  | 
|
| 59319 | 156  | 
def is_absolute: Boolean = elems.nonEmpty && elems.last.isInstanceOf[Path.Root]  | 
| 65559 | 157  | 
  def is_root: Boolean = elems match { case List(Path.Root(_)) => true case _ => false }
 | 
| 43600 | 158  | 
  def is_basic: Boolean = elems match { case List(Path.Basic(_)) => true case _ => false }
 | 
159  | 
||
| 45244 | 160  | 
def +(other: Path): Path = new Path((other.elems :\ elems)(Path.apply_elem))  | 
| 43600 | 161  | 
|
162  | 
||
| 43604 | 163  | 
/* implode */  | 
| 43600 | 164  | 
|
| 56844 | 165  | 
private def gen_implode(short: Boolean): String =  | 
| 43600 | 166  | 
    elems match {
 | 
167  | 
case Nil => "."  | 
|
168  | 
      case List(Path.Root("")) => "/"
 | 
|
| 56844 | 169  | 
      case _ => elems.map(Path.implode_elem(_, short)).reverse.mkString("/")
 | 
| 43600 | 170  | 
}  | 
| 56844 | 171  | 
def implode: String = gen_implode(false)  | 
172  | 
def implode_short: String = gen_implode(true)  | 
|
| 43600 | 173  | 
|
| 43652 | 174  | 
override def toString: String = quote(implode)  | 
| 43600 | 175  | 
|
176  | 
||
177  | 
/* base element */  | 
|
178  | 
||
179  | 
private def split_path: (Path, String) =  | 
|
180  | 
    elems match {
 | 
|
| 45244 | 181  | 
case Path.Basic(s) :: xs => (new Path(xs), s)  | 
| 43604 | 182  | 
      case _ => error("Cannot split path into dir/base: " + toString)
 | 
| 43600 | 183  | 
}  | 
184  | 
||
185  | 
def dir: Path = split_path._1  | 
|
| 45244 | 186  | 
def base: Path = new Path(List(Path.Basic(split_path._2)))  | 
| 43600 | 187  | 
|
188  | 
def ext(e: String): Path =  | 
|
189  | 
if (e == "") this  | 
|
190  | 
    else {
 | 
|
191  | 
val (prfx, s) = split_path  | 
|
| 43604 | 192  | 
prfx + Path.basic(s + "." + e)  | 
| 43600 | 193  | 
}  | 
| 43604 | 194  | 
|
| 53336 | 195  | 
def backup: Path =  | 
196  | 
  {
 | 
|
197  | 
val (prfx, s) = split_path  | 
|
198  | 
prfx + Path.basic(s + "~")  | 
|
199  | 
}  | 
|
200  | 
||
| 58610 | 201  | 
def backup2: Path =  | 
202  | 
  {
 | 
|
203  | 
val (prfx, s) = split_path  | 
|
204  | 
prfx + Path.basic(s + "~~")  | 
|
205  | 
}  | 
|
206  | 
||
| 
43697
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
207  | 
  private val Ext = new Regex("(.*)\\.([^.]*)")
 | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
208  | 
|
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
209  | 
def split_ext: (Path, String) =  | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
210  | 
  {
 | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
211  | 
val (prefix, base) = split_path  | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
212  | 
    base match {
 | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
213  | 
case Ext(b, e) => (prefix + Path.basic(b), e)  | 
| 56556 | 214  | 
case _ => (prefix + Path.basic(base), "")  | 
| 
43697
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
215  | 
}  | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
216  | 
}  | 
| 
 
77ce24aa1770
explicit Document.Node.Header, with master_dir and thy_name;
 
wenzelm 
parents: 
43670 
diff
changeset
 | 
217  | 
|
| 69367 | 218  | 
def drop_ext: Path = split_ext._1  | 
219  | 
def get_ext: String = split_ext._2  | 
|
220  | 
||
| 43604 | 221  | 
|
222  | 
/* expand */  | 
|
223  | 
||
| 
64228
 
b46969a851a9
expand relatively to given environment, notably remote HOME;
 
wenzelm 
parents: 
63866 
diff
changeset
 | 
224  | 
def expand_env(env: Map[String, String]): Path =  | 
| 43604 | 225  | 
  {
 | 
226  | 
def eval(elem: Path.Elem): List[Path.Elem] =  | 
|
227  | 
      elem match {
 | 
|
| 43664 | 228  | 
case Path.Variable(s) =>  | 
| 
64228
 
b46969a851a9
expand relatively to given environment, notably remote HOME;
 
wenzelm 
parents: 
63866 
diff
changeset
 | 
229  | 
val path = Path.explode(Isabelle_System.getenv_strict(s, env))  | 
| 48658 | 230  | 
if (path.elems.exists(_.isInstanceOf[Path.Variable]))  | 
| 53046 | 231  | 
            error("Illegal path variable nesting: " + s + "=" + path.toString)
 | 
| 48658 | 232  | 
else path.elems  | 
| 43604 | 233  | 
case x => List(x)  | 
234  | 
}  | 
|
235  | 
||
| 45244 | 236  | 
new Path(Path.norm_elems(elems.map(eval).flatten))  | 
| 43604 | 237  | 
}  | 
| 48373 | 238  | 
|
| 
64228
 
b46969a851a9
expand relatively to given environment, notably remote HOME;
 
wenzelm 
parents: 
63866 
diff
changeset
 | 
239  | 
def expand: Path = expand_env(Isabelle_System.settings())  | 
| 
 
b46969a851a9
expand relatively to given environment, notably remote HOME;
 
wenzelm 
parents: 
63866 
diff
changeset
 | 
240  | 
|
| 69366 | 241  | 
def file_name: String = expand.base.implode  | 
242  | 
||
| 48373 | 243  | 
|
| 48548 | 244  | 
/* source position */  | 
245  | 
||
246  | 
def position: Position.T = Position.File(implode)  | 
|
247  | 
||
248  | 
||
| 66232 | 249  | 
/* platform files */  | 
| 48373 | 250  | 
|
| 60988 | 251  | 
def file: JFile = File.platform_file(this)  | 
| 48548 | 252  | 
def is_file: Boolean = file.isFile  | 
253  | 
def is_dir: Boolean = file.isDirectory  | 
|
| 65833 | 254  | 
|
| 66232 | 255  | 
def absolute_file: JFile = File.absolute(file)  | 
256  | 
def canonical_file: JFile = File.canonical(file)  | 
|
| 67181 | 257  | 
|
258  | 
def absolute: Path = File.path(absolute_file)  | 
|
259  | 
def canonical: Path = File.path(canonical_file)  | 
|
| 43600 | 260  | 
}  |