|
| 1 | +//! Roundtrip example: decode, inspect, manipulate, and re-encode source map mappings. |
| 2 | +//! |
| 3 | +//! Simulates what a build tool does when it needs to inspect or transform |
| 4 | +//! individual mappings — for example, prepending whitespace to every generated |
| 5 | +//! line and adjusting the source map to match. |
| 6 | +//! |
| 7 | +//! Run with: cargo run -p srcmap-codec --example roundtrip |
| 8 | +
|
| 9 | +use srcmap_codec::{DecodeError, Segment, decode, encode}; |
| 10 | + |
| 11 | +fn main() { |
| 12 | + // ----------------------------------------------------------------------- |
| 13 | + // 1. Decode a realistic mappings string |
| 14 | + // ----------------------------------------------------------------------- |
| 15 | + // |
| 16 | + // This mappings string represents 5 generated lines from a small bundled |
| 17 | + // module. It uses a mix of segment types: |
| 18 | + // |
| 19 | + // - 1-field segments: only a generated column (no source mapping) |
| 20 | + // - 4-field segments: generated column + source/line/column |
| 21 | + // - 5-field segments: same as 4-field + a name index |
| 22 | + // |
| 23 | + // ECMA-426 VLQ mappings are delta-encoded on the wire: |
| 24 | + // - Generated column resets to 0 at each new line (`;`) |
| 25 | + // - Source index, original line, original column, and name index |
| 26 | + // are cumulative across the entire string |
| 27 | + // |
| 28 | + // After decoding, segments contain ABSOLUTE values — the codec handles |
| 29 | + // all delta accumulation internally. |
| 30 | + // |
| 31 | + // Line 0: two 4-field segments — "AAAA,IAAA" |
| 32 | + // (col 0 -> src 0, line 0, col 0) and (col 4 -> src 0, line 0, col 0) |
| 33 | + // Line 1: one 5-field segment with a name — "EACEC" |
| 34 | + // (col 2 -> src 0, line 1, col 2, name 1) |
| 35 | + // Line 2: one 4-field segment — "IAEE" |
| 36 | + // (col 4 -> src 0, line 3, col 4) |
| 37 | + // Line 3: empty line (no mappings) |
| 38 | + // Line 4: one 1-field segment (col 0, no source info) — "A" |
| 39 | + |
| 40 | + let mappings_str = "AAAA,IAAA;EACEC;IAEE;;A"; |
| 41 | + |
| 42 | + println!("=== Source Map Codec Roundtrip ===\n"); |
| 43 | + println!("Input mappings: {mappings_str:?}\n"); |
| 44 | + |
| 45 | + let mappings = decode(mappings_str).expect("valid mappings string"); |
| 46 | + |
| 47 | + // ----------------------------------------------------------------------- |
| 48 | + // 2. Inspect the decoded structure |
| 49 | + // ----------------------------------------------------------------------- |
| 50 | + |
| 51 | + println!("Decoded {} lines:\n", mappings.len()); |
| 52 | + |
| 53 | + for (line_idx, line) in mappings.iter().enumerate() { |
| 54 | + if line.is_empty() { |
| 55 | + println!(" Line {line_idx}: (empty)"); |
| 56 | + continue; |
| 57 | + } |
| 58 | + |
| 59 | + println!(" Line {line_idx}: {} segment(s)", line.len()); |
| 60 | + |
| 61 | + for (seg_idx, segment) in line.iter().enumerate() { |
| 62 | + // Segment implements Deref<Target=[i64]>, so len() and indexing work |
| 63 | + match segment.len() { |
| 64 | + 1 => { |
| 65 | + println!(" [{seg_idx}] 1-field: generated_col={}", segment[0],); |
| 66 | + } |
| 67 | + 4 => { |
| 68 | + println!( |
| 69 | + " [{seg_idx}] 4-field: generated_col={}, source={}, orig_line={}, orig_col={}", |
| 70 | + segment[0], segment[1], segment[2], segment[3], |
| 71 | + ); |
| 72 | + } |
| 73 | + 5 => { |
| 74 | + println!( |
| 75 | + " [{seg_idx}] 5-field: generated_col={}, source={}, orig_line={}, orig_col={}, name={}", |
| 76 | + segment[0], segment[1], segment[2], segment[3], segment[4], |
| 77 | + ); |
| 78 | + } |
| 79 | + other => { |
| 80 | + println!(" [{seg_idx}] unexpected {other}-field segment"); |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + // ----------------------------------------------------------------------- |
| 87 | + // 3. Manipulate: shift all generated columns by +4 |
| 88 | + // ----------------------------------------------------------------------- |
| 89 | + // |
| 90 | + // This simulates a build tool that prepends " " (4 spaces) to every |
| 91 | + // generated line. Each segment's generated column (field 0) increases by 4. |
| 92 | + // Source-side fields are unchanged — they still point to the original code. |
| 93 | + |
| 94 | + println!("\n--- Shifting all generated columns by +4 ---\n"); |
| 95 | + |
| 96 | + let shifted: Vec<Vec<Segment>> = mappings |
| 97 | + .iter() |
| 98 | + .map(|line| { |
| 99 | + line.iter() |
| 100 | + .map(|seg| match seg.len() { |
| 101 | + 1 => Segment::one(seg[0] + 4), |
| 102 | + 4 => Segment::four(seg[0] + 4, seg[1], seg[2], seg[3]), |
| 103 | + 5 => Segment::five(seg[0] + 4, seg[1], seg[2], seg[3], seg[4]), |
| 104 | + _ => *seg, |
| 105 | + }) |
| 106 | + .collect() |
| 107 | + }) |
| 108 | + .collect(); |
| 109 | + |
| 110 | + // ----------------------------------------------------------------------- |
| 111 | + // 4. Re-encode and verify roundtrip |
| 112 | + // ----------------------------------------------------------------------- |
| 113 | + |
| 114 | + let encoded_original = encode(&mappings); |
| 115 | + let encoded_shifted = encode(&shifted); |
| 116 | + |
| 117 | + println!("Original re-encoded: {encoded_original:?}"); |
| 118 | + println!("Shifted re-encoded: {encoded_shifted:?}\n"); |
| 119 | + |
| 120 | + // Verify the original roundtrips exactly |
| 121 | + assert_eq!( |
| 122 | + encoded_original, mappings_str, |
| 123 | + "roundtrip must produce identical output" |
| 124 | + ); |
| 125 | + println!("Roundtrip verified: original encodes back to the same string."); |
| 126 | + |
| 127 | + // Verify the shifted version decodes correctly |
| 128 | + let re_decoded = decode(&encoded_shifted).expect("shifted mappings should be valid"); |
| 129 | + |
| 130 | + for (line_idx, (orig_line, shifted_line)) in mappings.iter().zip(re_decoded.iter()).enumerate() |
| 131 | + { |
| 132 | + assert_eq!( |
| 133 | + orig_line.len(), |
| 134 | + shifted_line.len(), |
| 135 | + "line {line_idx} segment count must match" |
| 136 | + ); |
| 137 | + for (seg_idx, (orig_seg, shifted_seg)) in |
| 138 | + orig_line.iter().zip(shifted_line.iter()).enumerate() |
| 139 | + { |
| 140 | + assert_eq!( |
| 141 | + shifted_seg[0], |
| 142 | + orig_seg[0] + 4, |
| 143 | + "line {line_idx} segment {seg_idx}: generated column must be shifted by 4" |
| 144 | + ); |
| 145 | + } |
| 146 | + } |
| 147 | + println!("Shift verified: all generated columns increased by 4.\n"); |
| 148 | + |
| 149 | + // ----------------------------------------------------------------------- |
| 150 | + // 5. Error handling for invalid input |
| 151 | + // ----------------------------------------------------------------------- |
| 152 | + |
| 153 | + println!("--- Error handling ---\n"); |
| 154 | + |
| 155 | + // Invalid base64 character |
| 156 | + match decode("AA!A") { |
| 157 | + Err(DecodeError::InvalidBase64 { byte, offset }) => { |
| 158 | + println!( |
| 159 | + "InvalidBase64: byte 0x{byte:02x} ({:?}) at offset {offset}", |
| 160 | + char::from(byte), |
| 161 | + ); |
| 162 | + } |
| 163 | + other => panic!("expected InvalidBase64, got {other:?}"), |
| 164 | + } |
| 165 | + |
| 166 | + // Truncated VLQ (continuation bit set, but no more input) |
| 167 | + match decode("g") { |
| 168 | + Err(DecodeError::UnexpectedEof { offset }) => { |
| 169 | + println!("UnexpectedEof: at offset {offset}"); |
| 170 | + } |
| 171 | + other => panic!("expected UnexpectedEof, got {other:?}"), |
| 172 | + } |
| 173 | + |
| 174 | + // Invalid segment length (2-field segments are not allowed per ECMA-426) |
| 175 | + match decode("AC") { |
| 176 | + Err(DecodeError::InvalidSegmentLength { fields, offset }) => { |
| 177 | + println!("InvalidSegmentLength: {fields} fields at offset {offset}"); |
| 178 | + } |
| 179 | + other => panic!("expected InvalidSegmentLength, got {other:?}"), |
| 180 | + } |
| 181 | + |
| 182 | + // VLQ overflow (too many continuation bytes) |
| 183 | + match decode("gggggggggggggg") { |
| 184 | + Err(DecodeError::VlqOverflow { offset }) => { |
| 185 | + println!("VlqOverflow: at offset {offset}"); |
| 186 | + } |
| 187 | + other => panic!("expected VlqOverflow, got {other:?}"), |
| 188 | + } |
| 189 | + |
| 190 | + println!("\nAll assertions passed."); |
| 191 | +} |
0 commit comments