///|
// Matching order adapted from Go path/filepath/match.go.
// Copyright 2010 The Go Authors. All rights reserved.
// BSD-style license: licenses/Go-BSD.txt.
// COPY sources use Go filepath.Match syntax (Linux separators), not gitignore
// rules or .dockerignore's recursive **. A small instruction-specific adapter:
// literals, ?, *, escaped literals and Go character ranges. No ignore engine.
priv enum CopyToken {
Literal(Char)
One
Many
Range(Bool, Array[(Char, Char)])
}
///|
fn copy_tokens(pattern : String) -> Array[CopyToken] raise ParseError {
let chars = pattern.to_array()
let tokens = []
let mut i = 0
fn escaped() -> Char raise ParseError {
if i >= chars.length() || chars[i] == '-' || chars[i] == ']' {
raise Invalid("invalid COPY source character class")
}
if chars[i] == '\\' {
i += 1
if i >= chars.length() {
raise Invalid("invalid COPY source escape")
}
}
let c = chars[i]
i += 1
c
}
while i < chars.length() {
let c = chars[i]
i += 1
match c {
'*' => {
while i < chars.length() && chars[i] == '*' {
i += 1
}
tokens.push(Many)
}
'?' => tokens.push(One)
'\\' => {
if i == chars.length() {
raise Invalid("invalid COPY source escape")
}
tokens.push(Literal(chars[i]))
i += 1
}
'[' => {
let negated = i < chars.length() && chars[i] == '^'
if negated {
i += 1
}
let ranges = []
while true {
let lo = escaped()
let hi = if i < chars.length() && chars[i] == '-' {
i += 1
escaped()
} else {
lo
}
ranges.push((lo, hi))
if i >= chars.length() {
raise Invalid("unclosed COPY source class")
}
if chars[i] == ']' {
i += 1
break
}
}
tokens.push(Range(negated, ranges))
}
_ => tokens.push(Literal(c))
}
}
tokens
}
///|
fn copy_match(
tokens : Array[CopyToken],
value : String,
descendants : Bool,
) -> Bool {
// Match Go's non-backtracking chunk order, including byte-wise star search.
// See THIRD-PARTY.md and Go's BSD license. Continuation-byte starting points
// decode as RuneError/1 in Go; retain them even though input is valid Unicode.
let cells : Array[(Char, Int)] = []
for c in value {
let n = if c.to_int() < 128 {
1
} else if c.to_int() < 2048 {
2
} else if c.to_int() < 65536 {
3
} else {
4
}
cells.push((c, n))
for _ in 1.. Int? {
let mut pos = at
for i in start..= limit {
return None
}
let (c, n) = cells[pos]
let matched = match tokens[i] {
Literal(want) => c == want && !(c == '�' && n == 1)
One => c != '/'
Range(negated, ranges) =>
ranges.iter().any(r => r.0 <= c && c <= r.1) != negated
Many => false
}
if !matched {
return None
}
pos += n
}
Some(pos)
}
fn exact(limit : Int) -> Bool {
let mut i = 0
let mut pos = 0
while i < tokens.length() {
let star = tokens[i] is Many
if star {
i += 1
}
if i == tokens.length() {
for j in pos..= limit || cells[at].0 == '/' {
break
}
at += 1
}
if !found {
return false
}
}
pos == limit
}
if exact(cells.length()) {
return true
}
if descendants {
for i in 0.. Bool raise ParseError {
if pattern.length() > 2048 ||
path.length() > 2048 ||
!valid_unicode(pattern) ||
!valid_unicode(path) {
raise Invalid("COPY pattern/path limit or Unicode")
}
copy_match(copy_tokens(pattern), path, false)
}
///|
fn context_path_valid(path : String) -> Bool {
!path.is_empty() &&
path.length() <= 2048 &&
valid_unicode(path) &&
!path.has_prefix("/") &&
!path.has_suffix("/") &&
!path.contains("\\") &&
path.iter().all(c => c.to_int() >= 32 && c.to_int() != 127) &&
path.split("/").all(part => part != "" && part != "." && part != "..")
}
///|
fn clean_copy_source(path : String) -> String {
let parts = []
for part in path.split("/") {
if part == ".." {
ignore(parts.pop())
} else if part != "" && part != "." {
parts.push(part.to_owned())
}
}
if parts.is_empty() {
"."
} else {
parts.join("/")
}
}