use serde::{Deserialize, Serialize};
use crate::types::{FileCoverage, Location};
use crate::{InstrumentOptions, instrument};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V8FunctionCoverage {
#[serde(rename = "functionName")]
pub function_name: String,
pub ranges: Vec<V8CoverageRange>,
#[serde(rename = "isBlockCoverage")]
pub is_block_coverage: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct V8CoverageRange {
#[serde(rename = "startOffset")]
pub start_offset: u32,
#[serde(rename = "endOffset")]
pub end_offset: u32,
pub count: u32,
}
#[derive(Debug)]
pub enum V8ToIstanbulError {
Parse(String),
}
impl std::fmt::Display for V8ToIstanbulError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(msg) => write!(f, "parse error: {msg}"),
}
}
}
impl std::error::Error for V8ToIstanbulError {}
pub fn v8_to_istanbul(
source: &str,
filename: &str,
functions: &[V8FunctionCoverage],
wrapper_length: u32,
) -> Result<FileCoverage, V8ToIstanbulError> {
let instrumented = instrument(source, filename, &InstrumentOptions::default())
.map_err(|e| V8ToIstanbulError::Parse(e.to_string()))?;
let mut file_coverage = instrumented.coverage_map;
let line_offsets = compute_line_offsets(source);
let ranges: Vec<V8CoverageRange> =
functions.iter().flat_map(|f| f.ranges.iter().copied()).collect();
for (id, loc) in &file_coverage.statement_map {
let count = count_for_location(source, loc, &line_offsets, &ranges, wrapper_length);
if let Some(slot) = file_coverage.s.get_mut(id) {
*slot = count;
}
}
for (id, fn_entry) in &file_coverage.fn_map {
let count =
count_for_location(source, &fn_entry.loc, &line_offsets, &ranges, wrapper_length);
if let Some(slot) = file_coverage.f.get_mut(id) {
*slot = count;
}
}
for (id, branch_entry) in &file_coverage.branch_map {
let arm_counts: Vec<u32> = branch_entry
.locations
.iter()
.map(|loc| arm_count_for_location(source, loc, &line_offsets, &ranges, wrapper_length))
.collect();
if let Some(slot) = file_coverage.b.get_mut(id) {
*slot = arm_counts;
}
}
if file_coverage.input_source_map.is_none()
&& let Some(inline_map) = extract_inline_source_map(source)
{
file_coverage.input_source_map = Some(inline_map);
}
Ok(file_coverage)
}
fn extract_inline_source_map(source: &str) -> Option<serde_json::Value> {
const NEEDLE: &str = "//# sourceMappingURL=data:application/json";
let idx = source.rfind(NEEDLE)?;
let line = source[idx..].lines().next()?;
let comma = line.find(',')?;
let payload = &line[comma + 1..];
let is_base64 = line[..comma].contains(";base64");
let json = if is_base64 {
let bytes = decode_base64(payload).ok()?;
String::from_utf8(bytes).ok()?
} else {
urlencoding_decode(payload).ok()?
};
serde_json::from_str(&json).ok()
}
fn decode_base64(input: &str) -> Result<Vec<u8>, ()> {
fn value(c: u8) -> Result<u8, ()> {
match c {
b'A'..=b'Z' => Ok(c - b'A'),
b'a'..=b'z' => Ok(c - b'a' + 26),
b'0'..=b'9' => Ok(c - b'0' + 52),
b'+' | b'-' => Ok(62),
b'/' | b'_' => Ok(63),
_ => Err(()),
}
}
let trimmed: Vec<u8> =
input.bytes().filter(|b| *b != b'=' && !b.is_ascii_whitespace()).collect();
let mut out = Vec::with_capacity(trimmed.len() * 3 / 4);
for chunk in trimmed.chunks(4) {
let n0 = value(chunk[0])?;
let n1 = value(chunk[1])?;
out.push((n0 << 2) | (n1 >> 4));
if let Some(&c2) = chunk.get(2) {
let n2 = value(c2)?;
out.push((n1 << 4) | (n2 >> 2));
if let Some(&c3) = chunk.get(3) {
let n3 = value(c3)?;
out.push((n2 << 6) | n3);
}
}
}
Ok(out)
}
fn urlencoding_decode(input: &str) -> Result<String, ()> {
let mut out = Vec::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = (bytes[i + 1] as char).to_digit(16).ok_or(())? as u8;
let lo = (bytes[i + 2] as char).to_digit(16).ok_or(())? as u8;
out.push((hi << 4) | lo);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
String::from_utf8(out).map_err(|_| ())
}
fn compute_line_offsets(source: &str) -> Vec<u32> {
let mut offsets = vec![0u32];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
let next = u32::try_from(i + 1).unwrap_or(u32::MAX);
offsets.push(next);
}
}
let end = u32::try_from(source.len()).unwrap_or(u32::MAX);
offsets.push(end);
offsets
}
fn position_to_byte_offset(
source: &str,
line_1based: u32,
col_utf16: u32,
line_offsets: &[u32],
) -> u32 {
if line_1based == 0 {
return 0;
}
let line_idx = (line_1based - 1) as usize;
if line_idx >= line_offsets.len() - 1 {
return *line_offsets.last().unwrap_or(&0);
}
let line_start = line_offsets[line_idx] as usize;
let line_end = line_offsets[line_idx + 1] as usize;
let line_bytes = source.get(line_start..line_end).unwrap_or("");
let mut utf16_remaining = col_utf16;
let mut byte_in_line = 0usize;
for ch in line_bytes.chars() {
if utf16_remaining == 0 {
break;
}
let units = ch.len_utf16() as u32;
if units > utf16_remaining {
break;
}
utf16_remaining -= units;
byte_in_line += ch.len_utf8();
}
u32::try_from(line_start + byte_in_line).unwrap_or(u32::MAX)
}
fn count_for_location(
source: &str,
loc: &Location,
line_offsets: &[u32],
ranges: &[V8CoverageRange],
wrapper_length: u32,
) -> u32 {
let start = position_to_byte_offset(source, loc.start.line, loc.start.column, line_offsets)
+ wrapper_length;
let end = position_to_byte_offset(source, loc.end.line, loc.end.column, line_offsets)
+ wrapper_length;
smallest_containing_range_count(start, end, ranges)
}
fn arm_count_for_location(
source: &str,
arm_loc: &Location,
line_offsets: &[u32],
ranges: &[V8CoverageRange],
wrapper_length: u32,
) -> u32 {
const TOLERANCE: u32 = 4;
let arm_start =
position_to_byte_offset(source, arm_loc.start.line, arm_loc.start.column, line_offsets)
+ wrapper_length;
let arm_end =
position_to_byte_offset(source, arm_loc.end.line, arm_loc.end.column, line_offsets)
+ wrapper_length;
let mut best: Option<(V8CoverageRange, u32)> = None;
for r in ranges {
let dist_start = r.start_offset.abs_diff(arm_start);
let dist_end = r.end_offset.abs_diff(arm_end);
if dist_start > TOLERANCE || dist_end > TOLERANCE {
continue;
}
let distance = dist_start + dist_end;
match best {
None => best = Some((*r, distance)),
Some((prev, prev_distance)) => {
let prev_width = prev.end_offset.saturating_sub(prev.start_offset);
let this_width = r.end_offset.saturating_sub(r.start_offset);
if distance < prev_distance
|| (distance == prev_distance && this_width < prev_width)
{
best = Some((*r, distance));
}
}
}
}
best.map_or(0, |(r, _)| r.count)
}
fn smallest_containing_range_count(start: u32, end: u32, ranges: &[V8CoverageRange]) -> u32 {
let mut best: Option<V8CoverageRange> = None;
for r in ranges {
if r.start_offset <= start && r.end_offset >= end {
let width = r.end_offset.saturating_sub(r.start_offset);
match best {
None => best = Some(*r),
Some(prev) => {
let prev_width = prev.end_offset.saturating_sub(prev.start_offset);
if width < prev_width {
best = Some(*r);
}
}
}
}
}
best.map_or(0, |r| r.count)
}