///|
fn help_message() -> String {
let message =
#|Usage: patch [OPTION]... [ORIGINALFILE [PATCHFILE]]
#|Apply unified or context patches with exact hunk positions and context.
#| -p, --strip=N remove N leading file-name components
#| -i, --input=FILE read the patch from FILE instead of stdin
#| -o, --output=- write one patched file to stdout
#| -d, --directory=DIR resolve target paths relative to DIR
#| -R, --reverse apply additions as removals and vice versa
#| --dry-run check without writing or deleting files
#| -s, --quiet suppress routine file messages
#| -u, --unified require unified input
#| -c, --context require context input
#| -t, --batch accept the noninteractive behavior
#| -F, --fuzz=0 only exact context is supported
#| --no-backup-if-mismatch accept the no-backup strict profile
#| --help display this help and exit
#| --version display version and exit
message
}
///|
fn stripped(name : String, count : Int?) -> String raise PatchError {
let pieces = name.split("/").filter(s => s != "").to_array()
let drop = count.unwrap_or((pieces.length() - 1).max(0))
if count == Some(0) && name.has_prefix("/") {
raise PatchError("absolute paths in patch headers are not supported")
}
if drop >= pieces.length() {
raise PatchError("file name has too few components for --strip")
}
let selected = pieces[drop:]
if selected.any(part => part == "." || part == ".." || part.contains("\\")) {
raise PatchError("unsafe file name in patch header")
}
selected.map(v => v.to_owned()).join("/")
}
///|
async fn run(args : ArrayView[String]) -> Int {
let parsed = @cli.parse(args, [
@cli.option("strip", short='p'),
@cli.option("input", short='i'),
@cli.option("output", short='o'),
@cli.option("directory", short='d'),
@cli.flag("reverse", short='R'),
@cli.flag("dry-run"),
@cli.flag("quiet", short='s'),
@cli.flag("unified", short='u'),
@cli.flag("context", short='c'),
@cli.flag("batch", short='t'),
@cli.option("fuzz", short='F'),
@cli.flag("no-backup-if-mismatch"),
@cli.flag("help"),
@cli.flag("version"),
])
if parsed.contains("help") {
@stdio.stdout.write(help_message() + "\n")
return 0
}
if parsed.contains("version") {
@stdio.stdout.write("patch \{command_version}\n")
return 0
}
if parsed.operands.length() > 2 {
raise PatchError("too many operands")
}
if parsed.last_value("fuzz").unwrap_or("0") != "0" {
raise PatchError("only exact context (--fuzz=0) is supported")
}
if parsed.last_value("output") is Some(path) && path != "-" {
raise PatchError(
"named output requires public source mode metadata; use --output=-",
)
}
if parsed.contains("unified") && parsed.contains("context") {
raise PatchError("select either unified or context input")
}
let strip = match parsed.last_value("strip") {
Some(value) => {
let n = @string.parse_int(value) catch {
_ => raise PatchError("invalid strip count")
}
if n < 0 {
raise PatchError("invalid strip count")
}
Some(n)
}
None => None
}
if parsed.operands.length() == 2 && parsed.contains("input") {
raise PatchError("patch file specified twice")
}
let input = parsed
.last_value("input")
.unwrap_or(
if parsed.operands.length() == 2 {
parsed.operands[1]
} else {
"-"
},
)
let data = if input == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(input).binary()
}
let patches = if parsed.contains("unified") {
parse_unified(data)
} else if parsed.contains("context") {
parse_context(data)
} else {
parse_patch(data)
}
let explicit : String? = if parsed.operands.is_empty() {
None
} else {
Some(parsed.operands[0])
}
if patches.length() > 1 && (explicit is Some(_) || parsed.contains("output")) {
raise PatchError(
"an explicit target or output requires a single file patch",
)
}
let reverse = parsed.contains("reverse")
let dry_run = parsed.contains("dry-run")
let directory = parsed.last_value("directory").unwrap_or(".")
let targets : Array[(String, String, Bool, Bool)] = []
// Validate all names before any patch writes. Exact matching is checked per file.
for patch in patches {
let old_name = if reverse { patch.new_name } else { patch.old_name }
let new_name = if reverse { patch.old_name } else { patch.new_name }
let create = old_name == "/dev/null"
let remove = new_name == "/dev/null"
if !dry_run &&
!create &&
!remove &&
parsed.last_value("output") != Some("-") {
raise PatchError(
"rewriting an existing file requires public source mode metadata; use --dry-run or --output=-",
)
}
if create && remove {
raise PatchError("both patch file names are /dev/null")
}
let label = match explicit {
Some(path) => path
None => stripped(if create { new_name } else { old_name }, strip)
}
let path = if directory == "." {
label
} else {
@fsops.join(directory, label)
}
let output = match parsed.last_value("output") {
Some("-") => "-"
Some(name) =>
if directory == "." {
name
} else {
@fsops.join(directory, name)
}
None => path
}
targets.push((label, path, create, remove))
if output != "-" {
let kind = @fsops.kind_if_exists(output)
if kind is Some(kind) && kind != @fs.FileKind::Regular {
raise PatchError("output is not a regular file: '\{output}'")
}
}
}
let mut failed = false
for index, patch in patches {
let (label, path, create, remove) = targets[index]
let kind = @fsops.kind_if_exists(path)
if create && kind is Some(_) {
raise PatchError("cannot create existing file '\{label}'")
}
if !create && kind != Some(@fs.FileKind::Regular) {
raise PatchError("input is not a regular file: '\{label}'")
}
let original = if create { b"" } else { @fs.read_file(path).binary() }
if !parsed.contains("quiet") {
@stdio.stdout.write(
(if dry_run { "checking file " } else { "patching file " }) +
label +
"\n",
)
}
let result = apply_patch_bytes(original, patch.hunks, reverse) catch {
HunkMismatch(number, position) => {
@stdio.stderr.write(
"patch: hunk #\{number} does not match at line \{position}; file unchanged\n",
)
failed = true
continue
}
}
if remove && !result.is_empty() {
raise PatchError("deletion patch leaves file content")
}
if dry_run {
continue
}
match parsed.last_value("output") {
Some("-") => @stdio.stdout.write(result)
Some(_) =>
raise PatchError(
"named output requires public source mode metadata; use --output=-",
)
None =>
if remove {
@fs.remove(path)
} else if create {
let parent = @fsops.dirname(path)
if parent != "." {
@fs.mkdir(parent, recursive=true, permission=0o777)
}
@fs.write_file(path, result, create_mode=CreateNew, permission=0o666)
} else {
raise PatchError(
"existing file updates require public source mode metadata",
)
}
}
}
if failed {
1
} else {
0
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("patch 0.1.1\n")
return
}
let status = run(@env.args()[1:]) catch {
PatchError(message) => {
@stdio.stderr.write("patch: \{message}\n")
2
}
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("patch: \{message}: '\{option}'\n")
2
}
err => {
@stdio.stderr.write("patch: \{err}\n")
2
}
}
if status != 0 {
@sys.exit(status)
}
}