// SPDX-License-Identifier: Apache-2.0
///|
fn mutate(seed : Bytes, index : Int32, word : UInt32) -> Bytes {
let operation = index / 5 % 10
let at = (word % seed.length().reinterpret_as_uint()).reinterpret_as_int()
if operation == 0 {
return seed[:at].to_owned()
}
if operation == 5 {
return seed + seed
}
if operation == 7 {
return @utf8.encode("#" + "x".repeat(1200) + "\n") + seed
}
if operation == 8 {
return seed
}
let out : Array[Byte] = []
for i in 0..> 16).to_byte())
}
if operation == 2 && i == at {
continue
}
out.push(
if operation == 1 && i == at {
(word >> 8).to_byte()
} else {
seed[i]
},
)
}
if operation == 4 {
out.push(b'\xff')
}
if operation == 9 {
for b in b"\n-2147483648 nan inf 1e309 4294967295\n" {
out.push(b)
}
}
Bytes::from_array(out)
}
///|
fn direct(
data : Bytes,
kind : Int32,
limits : @stlkit.ParseLimits,
) -> Result[@stlkit.Mesh, String] {
match kind {
0 => @stlkit.parse_stl_binary(data, limits~)
1 => @stlkit.parse_stl_ascii(@utf8.decode_lossy(data), limits~)
2 => @stlkit.parse_obj(@utf8.decode_lossy(data), limits~)
3 => @stlkit.parse_ply(data, limits~)
_ => @stlkit.parse_3mf(data, limits~)
}
}
///|
fn main raise {
let args = @env.args()
let offset = argument_offset()
let count = if args.length() > offset {
@string.parse_int(args[offset])
} else {
1000
}
let start = if args.length() > offset + 1 {
@string.parse_int(args[offset + 1])
} else {
0
}
guard count > 0 && count <= 10000 && start >= 0 && start <= 10000 else {
fail("count 1..10000; start 0..10000")
}
let limits : @stlkit.ParseLimits = {
max_input_bytes: 8192,
max_vertices: 256,
max_triangles: 128,
max_list_items: 2048,
max_polygon_vertices: 64,
max_uncompressed_bytes: 32768,
max_zip_entries: 16,
max_component_depth: 8,
max_component_instances: 256,
max_records: 8192,
max_line_bytes: 1024,
max_xml_depth: 32,
}
let seeds = seeds()
let mut random = 0x53544c4bU
let mut digest = 2166136261U
let mut accepted = 0
let mut rejected = 0
for index in 0..<(start + count) {
random = random * 1664525U + 1013904223U
if index < start {
continue
}
let kind = index % 5
let input = if index / 5 % 10 == 6 {
targeted_seed(kind, index)
} else {
mutate(seeds[kind], index, random)
}
// Last line before a crash identifies the exact reproducible case.
println("CASE \{index} kind=\{kind} word=\{random} bytes=\{input.length()}")
let result = direct(input, kind, limits)
let automatic = @stlkit.parse_mesh(input, limits~)
match (automatic, @stlkit.parse_mesh_checked(input, limits~)) {
(Ok(mesh), Ok(checked)) =>
assert_eq(mesh.triangle_count(), checked.triangle_count())
(Err(_), Err(error)) => assert_true(error.code().length() > 0)
_ => fail("checked and legacy parser acceptance differ")
}
let value = match result {
Ok(mesh) => {
accepted += 1
assert_true(mesh.triangle_count() <= limits.max_triangles)
assert_true(
automatic is Ok(_),
msg="automatic routing rejected a directly accepted seed",
)
for triangle in mesh.triangles {
for vertex in [triangle.a, triangle.b, triangle.c] {
assert_true(!vertex.x.is_nan() && !vertex.x.is_inf())
assert_true(!vertex.y.is_nan() && !vertex.y.is_inf())
assert_true(!vertex.z.is_nan() && !vertex.z.is_inf())
}
}
mesh.triangle_count() + 1
}
Err(message) => {
assert_true(message.length() > 0)
rejected += 1
0
}
}
if kind == 0 {
let scanner = @stlkit.StlScanner::new(limits~)
let split = input.length() / 2
scanner.feed(input[:split].to_owned())
scanner.feed(input[split:].to_owned())
match (result, scanner.finish()) {
(Ok(mesh), Ok(scan)) =>
assert_eq(mesh.triangle_count(), scan.triangle_count)
(Err(_), Err(_)) => ()
_ => fail("binary full/chunked acceptance differs")
}
}
digest = (digest ^ value.reinterpret_as_uint()) * 16777619U
}
println(
"FUZZ_RESULT start=\{start} cases=\{count} accepted=\{accepted} rejected=\{rejected} digest=\{digest}",
)
}
///|
#cfg(target="js")
fn argument_offset() -> Int32 {
2
}
///|
#cfg(not(target="js"))
fn argument_offset() -> Int32 {
1
}