///|
// Strategy: tac-spool
let block_size : Int32 = 65536
///|
// Some regular pseudo files report zero or a page-sized nominal length. Probe
// before output begins; an unknown extent must be consumed as a stream.
async fn has_extent(file : @fs.File, length : Int64) -> Bool {
let probe = FixedArray::make(1, b'\x00')
if length == 0 {
file.read_at(probe, position=0L) == 0
} else {
file.read_at(probe, position=length - 1L) == 1
}
}
///|
/// KMP in the direction of traversal avoids rescanning long shared separator
/// prefixes and carries matches across backward block boundaries.
fn reverse_failure(separator : Bytes) -> FixedArray[Int32] {
let failure = FixedArray::make(separator.length(), 0)
let mut matched = 0
for index in 1.. 0 && byte != separator[separator.length() - matched - 1] {
matched = failure[matched - 1]
}
if byte == separator[separator.length() - matched - 1] {
matched += 1
}
failure[index] = matched
}
failure
}
///|
async fn reverse_file(
file : @fs.File,
separator : Bytes,
before : Bool,
writer~ : &@io.Writer = @stdio.stdout,
size~ : Int32 = block_size,
) -> Unit {
if separator.length() == 1 {
reverse_byte_file(file, separator, before, writer, size)
return
}
let length = file.size()
let writer = @io.BufferedWriter::new(writer, size=block_size)
let scan = FixedArray::make(size, b'\x00')
let output = FixedArray::make(size, b'\x00')
let batch = @buffer.Buffer(size_hint=size)
let matches : Array[Int32] = []
let failure = reverse_failure(separator)
let mut past_end = length
let mut block_end = length
let matched = Ref(0)
while block_end > 0 {
let block_start = (block_end - size.to_int64()).max(0L)
let count = (block_end - block_start).to_int()
if file.read_at(scan, position=block_start, len=count) != count {
raise @io.ReaderClosed
}
let data = scan.unsafe_reinterpret_as_bytes()[:count]
reverse_block_matches(data, separator, failure, matched, matches)
let offset = if before { 0 } else { separator.length() }
let mut index = 0
// A separator can end beyond this block. Keep copying spanning ranges
// until both endpoints are local; only then is the scan buffer sufficient.
while index < matches.length() && past_end > block_end {
let boundary = block_start + (matches[index] + offset).to_int64()
@tempio.copy_range(file, boundary, past_end, output, writer)
past_end = boundary
index += 1
}
if index < matches.length() {
past_end = block_start +
reverse_matched_ranges(
data,
matches,
index,
offset,
(past_end - block_start).to_int(),
batch,
).to_int64()
if !batch.is_empty() {
let bytes = batch.to_bytes()
batch.reset()
writer.write(bytes)
}
}
block_end = block_start
}
@tempio.copy_range(file, 0L, past_end, output, writer)
writer.flush()
}
///|
// The first boundary in a block may complete a range from a later block.
// Every remaining range is local: copy those synchronously, then write once.
fn reverse_byte_ranges(
data : BytesView,
separator : Bytes,
before : Bool,
last : Int32,
output : @buffer.Buffer,
) -> Int32 {
let mut search_end = last
let mut end = last + (if before { 0 } else { 1 })
while data[:search_end].rev_find(separator) is Some(index) {
let boundary = index + (if before { 0 } else { 1 })
output.write_bytes(data[boundary:end])
end = boundary
search_end = index
}
end
}
///|
async fn reverse_byte_file(
file : @fs.File,
separator : Bytes,
before : Bool,
writer : &@io.Writer,
size : Int32,
) -> Unit {
let length = file.size()
let writer = @io.BufferedWriter::new(writer, size=block_size)
let scan = FixedArray::make(size, b'\x00')
let range = FixedArray::make(size, b'\x00')
let batch = @buffer.Buffer(size_hint=size)
let mut past_end = length
let mut block_end = length
let mut first = true
while block_end > 0 {
let block_start = (block_end - size.to_int64()).max(0L)
let count = (block_end - block_start).to_int()
if file.read_at(scan, position=block_start, len=count) != count {
raise @io.ReaderClosed
}
let data = scan.unsafe_reinterpret_as_bytes()[:count]
if data.rev_find(separator) is Some(last) {
let boundary = block_start +
(last + (if before { 0 } else { 1 })).to_int64()
if before || !first || boundary != past_end {
if past_end <= block_end {
writer.write(
data[(boundary - block_start).to_int():(past_end - block_start).to_int()],
)
} else {
@tempio.copy_range(file, boundary, past_end, range, writer)
}
}
past_end = block_start +
reverse_byte_ranges(data, separator, before, last, batch).to_int64()
first = false
if !batch.is_empty() {
let bytes = batch.to_bytes()
batch.reset()
writer.write(bytes)
}
}
block_end = block_start
}
@tempio.copy_range(file, 0L, past_end, range, writer)
writer.flush()
}
///|
async fn reverse_regex(
file : @fs.File,
pattern : @bytere.Pattern,
before : Bool,
writer : &@io.Writer,
) -> Unit {
// The matcher takes a contiguous byte-view String. Keep its O(input) cost
// explicit; ranges are emitted from the immutable spool without an output copy.
let text = @bytere.text(file.read_all().binary())
let writer = @io.BufferedWriter::new(writer, size=block_size)
let output = FixedArray::make(block_size, b'\x00')
let mut search_end = text.length()
let mut past_end = search_end
let mut first = true
while search_end > 0 {
guard pattern.rfind_span(text, search_end) is Some((start, end)) else {
break
}
let boundary = if before { start } else { end }
if before || !first || boundary != past_end {
@tempio.copy_range(
file,
boundary.to_int64(),
past_end.to_int64(),
output,
writer,
)
}
past_end = boundary
search_end = start
first = false
}
@tempio.copy_range(file, 0L, past_end.to_int64(), output, writer)
writer.flush()
}
///|
async fn reverse_spooled(
reader : &@io.Reader,
separator : Bytes,
before : Bool,
regex~ : @bytere.Pattern? = None,
writer~ : &@io.Writer = @stdio.stdout,
) -> Unit {
let directory = @tempio.Directory::new(prefix="cli-tac-")
with_spool_cleanup(
() => {
let file = @fs.open(
directory.path() + "/input",
mode=ReadWrite,
create_mode=CreateNew,
permission=0o600,
)
defer file.close()
let buffer = FixedArray::make(block_size, b'\x00')
while reader.read(buffer) is count && count > 0 {
file.write(buffer.unsafe_reinterpret_as_bytes()[:count])
}
match regex {
None => reverse_file(file, separator, before, writer~)
Some(pattern) => reverse_regex(file, pattern, before, writer)
}
},
() => directory.close(),
)
}
///|
// The operation owns its file handles and closes them before directory cleanup.
// A cleanup-only failure is observable; a primary failure keeps its diagnostic.
async fn with_spool_cleanup(
operation : async () -> Unit,
cleanup : async () -> Unit,
) -> Unit {
{
errdefer (cleanup() catch { _ => () })
operation()
}
cleanup()
}