# srcmap-sourcemap

[![crates.io](https://img.shields.io/crates/v/srcmap-sourcemap.svg)](https://crates.io/crates/srcmap-sourcemap)
[![docs.rs](https://docs.rs/srcmap-sourcemap/badge.svg)](https://docs.rs/srcmap-sourcemap)
[![CI](https://github.com/fallow-rs/srcmap/actions/workflows/ci.yml/badge.svg)](https://github.com/fallow-rs/srcmap/actions/workflows/ci.yml)
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/fallow-rs/srcmap/badges/rust-coverage.json)](https://github.com/fallow-rs/srcmap/actions/workflows/coverage.yml)

High-performance source map parser and consumer for Rust.

Parses source map JSON and provides O(log n) position lookups. Implements the [ECMA-426](https://tc39.es/ecma426/) Source Map v3 specification. Drop-in Rust equivalent of [`@jridgewell/trace-mapping`](https://github.com/jridgewell/trace-mapping).

## Install

```toml
[dependencies]
srcmap-sourcemap = "0.3"
```

## Usage

```rust
use srcmap_sourcemap::SourceMap;

let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
let sm = SourceMap::from_json(json).unwrap();

// Forward lookup: generated position -> original position (0-based)
let loc = sm.original_position_for(0, 0).unwrap();
assert_eq!(sm.source(loc.source), "input.js");
assert_eq!(loc.line, 0);
assert_eq!(loc.column, 0);

// Reverse lookup: original position -> generated position
let pos = sm.generated_position_for("input.js", 0, 0).unwrap();
assert_eq!(pos.line, 0);
assert_eq!(pos.column, 0);
```

## API

### `SourceMap`

| Method | Description |
|--------|-------------|
| `from_json(json) -> Result<SourceMap>` | Parse a source map from a JSON string (regular or indexed) |
| `from_json_no_content(json) -> Result<SourceMap>` | Parse JSON without allocating `sourcesContent` strings |
| `from_parts(file, source_root, sources, ...) -> SourceMap` | Build from pre-decoded components and mappings |
| `from_parts_with_extensions(file, source_root, sources, ..., extensions) -> SourceMap` | Build from pre-decoded components while preserving `x_` / `x-` extension fields |
| `from_vlq(mappings_str, sources, names, ...) -> Result<SourceMap>` | Build from pre-parsed components and a VLQ mappings string |
| `from_vlq_with_range_mappings(mappings_str, ..., range_mappings_str) -> Result<SourceMap>` | Build from VLQ mappings and optional range mappings strings |
| `from_json_lines(json, start_line, end_line) -> Result<SourceMap>` | Parse and decode only the given generated-line range |
| `builder() -> SourceMapBuilder` | Create a builder for constructing a `SourceMap` step by step |
| `original_position_for(line, col) -> Option<OriginalLocation>` | Look up original position for a generated position |
| `original_position_for_with_bias(line, col, bias) -> Option<OriginalLocation>` | Look up original position with explicit search bias |
| `generated_position_for(source, line, col) -> Option<GeneratedLocation>` | Reverse lookup: original position to generated position |
| `generated_position_for_with_bias(source, line, col, bias) -> Option<GeneratedLocation>` | Reverse lookup with explicit search bias |
| `all_generated_positions_for(source, line, col) -> Vec<GeneratedLocation>` | Find all generated positions for an original position |
| `map_range(start_line, start_col, end_line, end_col) -> Option<MappedRange>` | Map a generated range to its original range |
| `all_mappings() -> &[Mapping]` | All decoded mappings |
| `mappings_for_line(line) -> &[Mapping]` | Slice of mappings for a single generated line |
| `source(index) -> &str` | Resolve a source index to its filename |
| `get_source(index) -> Option<&str>` | Resolve a source index to its filename, or `None` if out of bounds |
| `name(index) -> &str` | Resolve a name index to its string |
| `get_name(index) -> Option<&str>` | Resolve a name index to its string, or `None` if out of bounds |
| `source_index(name) -> Option<u32>` | Look up a source filename to its index |
| `line_count() -> usize` | Number of generated lines |
| `mapping_count() -> usize` | Total number of decoded mappings |
| `has_range_mappings() -> bool` | Whether any mappings are range mappings |
| `range_mapping_count() -> usize` | Number of range mappings |
| `encode_mappings() -> String` | Encode all mappings to a VLQ mappings string |
| `encode_range_mappings() -> Option<String>` | Encode range mappings to VLQ string |
| `to_json() -> String` | Serialize the source map to JSON |
| `to_json_with_options(exclude_content) -> String` | Serialize to JSON, optionally excluding `sourcesContent` |

### `LazySourceMap`

| Method | Description |
|--------|-------------|
| `from_json(json) -> Result<LazySourceMap>` | Parse JSON eagerly but defer VLQ decoding |
| `from_json_no_content(json) -> Result<LazySourceMap>` | Parse JSON without allocating `sourcesContent` strings |
| `from_json_fast(json) -> Result<LazySourceMap>` | Fast-scan mode: skips `sourcesContent`, only byte-scans for semicolons |
| `from_vlq(mappings, sources, names, ...) -> Result<LazySourceMap>` | Build from pre-parsed components and a VLQ mappings string |
| `decode_line(line) -> Result<Vec<Mapping>>` | Decode mappings for a single generated line on demand |
| `original_position_for(line, col) -> Option<OriginalLocation>` | Look up original position (decodes the line lazily) |
| `line_count() -> usize` | Number of generated lines |
| `source(index) -> &str` | Resolve a source index to its filename |
| `get_source(index) -> Option<&str>` | Resolve a source index to its filename, or `None` if out of bounds |
| `name(index) -> &str` | Resolve a name index to its string |
| `get_name(index) -> Option<&str>` | Resolve a name index to its string, or `None` if out of bounds |
| `source_index(name) -> Option<u32>` | Look up a source filename to its index |
| `mappings_for_line(line) -> Vec<Mapping>` | Decoded mappings for a single generated line |
| `into_sourcemap() -> Result<SourceMap>` | Fully decode and convert to a `SourceMap` |

### Types

| Type | Description |
|------|-------------|
| `Mapping` | A single decoded mapping entry (28 bytes) |
| `OriginalLocation` | Result of a forward lookup (source, line, column, name) |
| `GeneratedLocation` | Result of a reverse lookup (line, column) |
| `GeneratedOffsetLookup<'a>` | Reusable V8 coverage byte-offset to generated-position lookup |
| `MappedRange` | Original start/end positions for a generated range |
| `Bias` | Search bias: `GreatestLowerBound` or `LeastUpperBound` |
| `MappingsIter<'a>` | Zero-allocation streaming iterator over VLQ-encoded mappings |
| `SourceMappingUrl` | Parsed sourceMappingURL: `Inline(String)` or `External(String)` |
| `ParseError` | Errors that can occur during source map parsing |

### Building From Decoded Parts

Compiler and coverage-instrumentation pipelines often already have source-map
parts in memory: generated file, sources, names, source content, decoded
mappings, ignore list, debug ID, and custom `x_` extension fields. Use
`SourceMap::from_parts_with_extensions` or `SourceMap::builder()` to pass those
parts directly into `srcmap-sourcemap` without serializing to source-map JSON and
parsing it back before lookup or `srcmap-remapping` composition.

For example, an Oxc-based coverage instrumenter can convert Oxc's emitted
source-map fields into a `SourceMap`, apply preamble offsetting, and compose
with an upstream `inputSourceMap` without using JSON as the interchange format.

```rust
use std::collections::HashMap;

use srcmap_sourcemap::{Mapping, SourceMap};

let mut extensions = HashMap::new();
extensions.insert("x_google_linecount".to_string(), serde_json::json!(1));

let sm = SourceMap::from_parts_with_extensions(
    Some("out.js".to_string()),
    None,
    vec!["src/input.ts".to_string()],
    vec![Some("export const value = 1;".to_string())],
    vec!["value".to_string()],
    vec![Mapping {
        generated_line: 0,
        generated_column: 0,
        source: 0,
        original_line: 0,
        original_column: 13,
        name: 0,
        is_range_mapping: false,
    }],
    vec![],
    None,
    None,
    extensions,
);

assert_eq!(sm.original_position_for(0, 0).unwrap().column, 13);
```

### Runtime Coverage Offsets

V8 coverage ranges commonly start from generated UTF-8 byte offsets. Source-map
lookups use generated `line,column` pairs where the column is a UTF-16 JavaScript
code-unit offset. Build one `GeneratedOffsetLookup` per generated asset and
reuse it for function ranges or beacon batches.

```rust
use srcmap_sourcemap::{GeneratedOffsetLookup, SourceMap};

let generated_code = "alpha();\nbeta();\n";
let map_json = r#"{
    "version": 3,
    "sources": ["input.ts"],
    "names": ["lineOne", "lineTwo"],
    "mappings": "AAAAA;AACAC"
}"#;

let sm = SourceMap::from_json(map_json).unwrap();
let lookup = GeneratedOffsetLookup::new(generated_code);

let start_offset = "alpha();\n".len() as u32;
let generated = lookup.byte_offset_to_position(start_offset).unwrap();
assert_eq!((generated.line, generated.column), (1, 0));

let original = lookup.original_position_for_offset(&sm, start_offset).unwrap();
assert_eq!(sm.source(original.source), "input.ts");
assert_eq!((original.line, original.column), (1, 0));
```

`GeneratedOffsetLookup` works with both `SourceMap` and `LazySourceMap` through
the `OriginalPositionLookup` trait.

### Free Functions

| Function | Description |
|----------|-------------|
| `parse_source_mapping_url(source) -> Option<SourceMappingUrl>` | Extract and parse a `sourceMappingURL` comment from source code |
| `validate_deep(sm) -> Vec<String>` | Deep structural validation with bounds and ordering checks |

### Fields

| Field | Type |
|-------|------|
| `sources` | `Vec<String>` |
| `sources_content` | `Vec<Option<String>>` |
| `names` | `Vec<String>` |
| `file` | `Option<String>` |
| `source_root` | `Option<String>` |
| `ignore_list` | `Vec<u32>` |
| `debug_id` | `Option<String>` |
| `extensions` | `HashMap<String, serde_json::Value>` |

## Features

- **O(log n) binary search** for both forward and reverse lookups
- **Flat 28-byte mapping structs** for cache-friendly iteration
- **Lazy reverse index** built on first `generated_position_for` call
- **Indexed source maps** (`sections`) with automatic flattening
- **Zero-copy JSON parsing** via borrowed mappings string
- **Range mappings** (`rangeMappings` field, ECMA-426 Stage 2 proposal) with cross-line delta lookup
- **Lazy iterator** (`MappingsIter`) for zero-allocation streaming over encoded mappings
- **Robust error handling** for malformed input

## Performance

Benchmarked on a 100K segment source map (Criterion):

| Operation | Time |
|-----------|------|
| Parse | 701 us |
| Single lookup | 3 ns |
| 1000x lookups | 5.8 us (5.8 ns/lookup) |

## Part of [srcmap](https://github.com/fallow-rs/srcmap)

This crate is the core of the srcmap source map toolkit. See also:
- [`srcmap-codec`](https://crates.io/crates/srcmap-codec) - VLQ encode/decode
- [`srcmap-generator`](https://crates.io/crates/srcmap-generator) - Source map builder
- [`srcmap-remapping`](https://crates.io/crates/srcmap-remapping) - Concatenation and composition

## License

MIT
