Skip to main content

delbin/
lib.rs

1//! # Delbin
2//!
3//! Descriptive Language for Binary Object
4//!
5//! Delbin is a Domain Specific Language (DSL) and its supporting library for describing
6//! and generating binary data structures, primarily used for Header information generation
7//! in embedded firmware packaging scenarios.
8//!
9//! ## Example
10//!
11//! ```rust
12//! use delbin::{generate, Value};
13//! use std::collections::HashMap;
14//!
15//! let dsl = r#"
16//!     @endian = little;
17//!     struct header @packed {
18//!         magic: [u8; 4] = @bytes("FPK\0");
19//!         version: u32 = ${VERSION};
20//!         size: u32 = @sizeof(image);
21//!     }
22//! "#;
23//!
24//! let mut env = HashMap::new();
25//! env.insert("VERSION".to_string(), Value::U64(0x0100));
26//!
27//! let mut sections = HashMap::new();
28//! sections.insert("image".to_string(), vec![0u8; 1024]);
29//!
30//! let result = generate(dsl, &env, &sections).unwrap();
31//! assert_eq!(result.data.len(), 12); // 4 + 4 + 4
32//! ```
33
34pub mod ast;
35pub mod builtin;
36pub mod error;
37pub mod eval;
38pub mod parser;
39pub mod types;
40pub mod utils;
41
42pub use error::{DelbinError, DelbinWarning, ErrorCode, Result, WarningCode};
43pub use types::{Endian, ScalarType, Value};
44pub use utils::{
45    create_env, create_sections, env_insert_int, env_insert_str, from_hex_string, hex_dump,
46    to_hex_string,
47};
48
49use std::collections::HashMap;
50
51/// Generation result
52#[derive(Debug)]
53pub struct GenerateResult {
54    /// Generated binary data
55    pub data: Vec<u8>,
56    /// Warning list
57    pub warnings: Vec<DelbinWarning>,
58}
59
60/// Generate binary data according to DSL definition
61///
62/// # Parameters
63///
64/// * `dsl` - DSL description text
65/// * `env` - Environment variable mapping
66/// * `sections` - External section data mapping
67///
68/// # Returns
69///
70/// Generated binary data and warning list
71///
72/// # Example
73///
74/// ```rust
75/// use delbin::{generate, Value};
76/// use std::collections::HashMap;
77///
78/// let dsl = r#"
79///     @endian = little;
80///     struct header @packed {
81///         magic: [u8; 4] = @bytes("TEST");
82///     }
83/// "#;
84///
85/// let env = HashMap::new();
86/// let sections = HashMap::new();
87///
88/// let result = generate(dsl, &env, &sections).unwrap();
89/// assert_eq!(&result.data[..4], b"TEST");
90/// ```
91pub fn generate(
92    dsl: &str,
93    env: &HashMap<String, Value>,
94    sections: &HashMap<String, Vec<u8>>,
95) -> Result<GenerateResult> {
96    // Parse DSL
97    let file = parser::parse(dsl)?;
98
99    // Evaluate
100    let mut evaluator = eval::Evaluator::new(env.clone(), sections.clone());
101    let data = evaluator.eval(&file)?;
102
103    Ok(GenerateResult {
104        data,
105        warnings: evaluator.warnings().to_vec(),
106    })
107}
108
109/// Generate hexadecimal string
110///
111/// # Parameters
112///
113/// * `dsl` - DSL description text
114/// * `env` - Environment variable mapping
115/// * `sections` - External section data mapping
116///
117/// # Returns
118///
119/// Hexadecimal string (uppercase, no separator)
120pub fn generate_hex(
121    dsl: &str,
122    env: &HashMap<String, Value>,
123    sections: &HashMap<String, Vec<u8>>,
124) -> Result<String> {
125    let result = generate(dsl, env, sections)?;
126    Ok(to_hex_string(&result.data))
127}
128
129/// Validate DSL without generating output
130///
131/// Checks syntax and semantics. Returns warnings on success, error on failure.
132pub fn validate(
133    dsl: &str,
134    env: &HashMap<String, Value>,
135) -> Result<Vec<DelbinWarning>> {
136    let file = parser::parse(dsl)?;
137    let mut evaluator = eval::Evaluator::new(env.clone(), HashMap::new());
138    evaluator.eval(&file)?;
139    Ok(evaluator.warnings().to_vec())
140}
141
142/// Parse binary data according to DSL field layout
143///
144/// Reverse of `generate()`. Extracts named field values from raw binary bytes.
145///
146/// # Parameters
147///
148/// * `dsl` - DSL description text
149/// * `env` - Environment variable mapping (needed to resolve dynamic sizes)
150/// * `data` - Raw binary bytes to parse
151///
152/// # Returns
153///
154/// Map of field name → value
155pub fn parse(
156    dsl: &str,
157    env: &HashMap<String, Value>,
158    data: &[u8],
159) -> Result<HashMap<String, Value>> {
160    let file = parser::parse(dsl)?;
161    let mut evaluator = eval::Evaluator::new(env.clone(), HashMap::new());
162    evaluator.parse_bytes(&file, data)
163}
164
165
166/// # Parameters
167///
168/// * `dsl` - DSL description text
169/// * `env` - Environment variable mapping
170/// * `image_data` - Target image data
171///
172/// # Returns
173///
174/// Merged data (header + image)
175pub fn merge(
176    dsl: &str,
177    env: &HashMap<String, Value>,
178    image_data: &[u8],
179) -> Result<GenerateResult> {
180    let mut sections = HashMap::new();
181    sections.insert("image".to_string(), image_data.to_vec());
182
183    let result = generate(dsl, env, &sections)?;
184
185    // Merge header and image
186    let mut merged = result.data;
187    merged.extend_from_slice(image_data);
188
189    Ok(GenerateResult {
190        data: merged,
191        warnings: result.warnings,
192    })
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_generate_simple() {
201        let dsl = r#"
202            @endian = little;
203            struct header @packed {
204                magic: [u8; 4] = @bytes("fpk\0");
205                version: u32 = 0x0100;
206            }
207        "#;
208
209        let env = HashMap::new();
210        let sections = HashMap::new();
211
212        let result = generate(dsl, &env, &sections).unwrap();
213        assert_eq!(result.data.len(), 8);
214        assert_eq!(&result.data[..4], b"fpk\0");
215        assert_eq!(&result.data[4..8], &[0x00, 0x01, 0x00, 0x00]);
216    }
217
218    #[test]
219    fn test_generate_with_env() {
220        let dsl = r#"
221            @endian = little;
222            struct header @packed {
223                version: u32 = (${MAJOR} << 24) | (${MINOR} << 16) | ${PATCH};
224            }
225        "#;
226
227        let mut env = HashMap::new();
228        env.insert("MAJOR".to_string(), Value::U64(1));
229        env.insert("MINOR".to_string(), Value::U64(2));
230        env.insert("PATCH".to_string(), Value::U64(3));
231
232        let sections = HashMap::new();
233
234        let result = generate(dsl, &env, &sections).unwrap();
235        assert_eq!(result.data, vec![0x03, 0x00, 0x02, 0x01]);
236    }
237
238    #[test]
239    fn test_generate_with_sizeof() {
240        let dsl = r#"
241            @endian = little;
242            struct header @packed {
243                img_size: u32 = @sizeof(image);
244            }
245        "#;
246
247        let env = HashMap::new();
248        let mut sections = HashMap::new();
249        sections.insert("image".to_string(), vec![0u8; 1024]);
250
251        let result = generate(dsl, &env, &sections).unwrap();
252        assert_eq!(result.data, vec![0x00, 0x04, 0x00, 0x00]); // 1024 = 0x400
253    }
254
255    #[test]
256    fn test_generate_with_crc32() {
257        let dsl = r#"
258            @endian = little;
259            struct header @packed {
260                crc: u32 = @crc32(image);
261            }
262        "#;
263
264        let env = HashMap::new();
265        let mut sections = HashMap::new();
266        sections.insert("image".to_string(), b"hello world".to_vec());
267
268        let result = generate(dsl, &env, &sections).unwrap();
269        // CRC32 of "hello world" = 0x0D4A1185
270        assert_eq!(result.data, vec![0x85, 0x11, 0x4A, 0x0D]);
271    }
272
273    #[test]
274    fn test_generate_with_self_sizeof() {
275        let dsl = r#"
276            @endian = little;
277            struct header @packed {
278                magic: [u8; 4] = @bytes("TEST");
279                header_size: u32 = @sizeof(@self);
280            }
281        "#;
282
283        let env = HashMap::new();
284        let sections = HashMap::new();
285
286        let result = generate(dsl, &env, &sections).unwrap();
287        assert_eq!(result.data.len(), 8);
288        // header_size = 8
289        assert_eq!(&result.data[4..8], &[0x08, 0x00, 0x00, 0x00]);
290    }
291
292    #[test]
293    fn test_generate_with_padding() {
294        let dsl = r#"
295            @endian = little;
296            struct header @packed {
297                magic: [u8; 4] = @bytes("TEST");
298                _pad: [u8; 64 - @offsetof(_pad)];
299            }
300        "#;
301
302        let env = HashMap::new();
303        let sections = HashMap::new();
304
305        let result = generate(dsl, &env, &sections).unwrap();
306        assert_eq!(result.data.len(), 64);
307    }
308
309    #[test]
310    fn test_generate_full_header() {
311        let dsl = r#"
312            @endian = little;
313            struct header @packed {
314                magic:          [u8; 4] = @bytes("fpk\0");
315                image_type:     u32 = 0;
316                header_ver:     u16 = 0x0100;
317                header_size:    u16 = @sizeof(@self);
318                fw_version:     u32 = (${VERSION_MAJOR} << 24) | (${VERSION_MINOR} << 16) | ${VERSION_PATCH};
319                build_number:   u32 = ${BUILD_NUMBER};
320                version_str:    [u8; 16] = @bytes(${VERSION_STRING});
321                flags:          u32 = 0;
322                img_size:       u32 = @sizeof(image);
323                packed_size:    u32 = @sizeof(image);
324                timestamp:      u32 = ${UNIX_STAMP};
325                partition:      [u8; 16] = @bytes("app");
326                watermark:      [u8; 16] = @bytes("DELBIN_DEMO");
327                reserved:       [u8; 32];
328                img_crc32:      u32 = @crc32(image);
329                img_sha256:     [u8; 32] = @sha256(image);
330                header_crc32:   u32 = @crc32(@self[..header_crc32]);
331                _padding:       [u8; 256 - @offsetof(_padding)];
332            }
333        "#;
334
335        let mut env = HashMap::new();
336        env.insert("VERSION_MAJOR".to_string(), Value::U64(1));
337        env.insert("VERSION_MINOR".to_string(), Value::U64(2));
338        env.insert("VERSION_PATCH".to_string(), Value::U64(3));
339        env.insert("BUILD_NUMBER".to_string(), Value::U64(100));
340        env.insert("VERSION_STRING".to_string(), Value::String("1.2.3".to_string()));
341        env.insert("UNIX_STAMP".to_string(), Value::U64(1705574400));
342
343        let mut sections = HashMap::new();
344        sections.insert("image".to_string(), vec![0xABu8; 1024]);
345
346        let result = generate(dsl, &env, &sections).unwrap();
347
348        // Verify total size
349        assert_eq!(result.data.len(), 256);
350
351        // Verify magic
352        assert_eq!(&result.data[0..4], b"fpk\0");
353
354        // Verify header_size (offset 10-11)
355        assert_eq!(result.data[10], 0x00); // 256 & 0xFF = 0
356        assert_eq!(result.data[11], 0x01); // 256 >> 8 = 1
357
358        println!("Generated header ({} bytes):", result.data.len());
359        println!("{}", hex_dump(&result.data, 16));
360    }
361
362    // ── Type-checking tests ────────────────────────────────────────────
363
364    #[test]
365    fn test_string_direct_assign_to_array_is_error() {
366        let dsl = r#"
367            @endian = little;
368            struct header @packed {
369                magic: [u8; 4] = "bad";
370            }
371        "#;
372        let result = generate(dsl, &HashMap::new(), &HashMap::new());
373        assert!(result.is_err(), "expected error for string literal directly assigned to array");
374        let msg = result.unwrap_err().message;
375        assert!(msg.contains("@bytes"), "error should mention @bytes, got: {}", msg);
376    }
377
378    #[test]
379    fn test_bytes_to_non_u8_array_is_error() {
380        let dsl = r#"
381            @endian = little;
382            struct header @packed {
383                data: [u16; 2] = @bytes("AB");
384            }
385        "#;
386        let result = generate(dsl, &HashMap::new(), &HashMap::new());
387        assert!(result.is_err(), "expected error for @bytes() on non-u8 array");
388        let msg = result.unwrap_err().message;
389        assert!(msg.contains("u8"), "error should mention u8, got: {}", msg);
390    }
391
392    #[test]
393    fn test_integer_truncation_emits_warning() {
394        let dsl = r#"
395            @endian = little;
396            struct header @packed {
397                small: u8 = 0x1FF;
398            }
399        "#;
400        let result = generate(dsl, &HashMap::new(), &HashMap::new()).unwrap();
401        assert_eq!(result.data, vec![0xFF]); // truncated
402        assert!(!result.warnings.is_empty(), "expected truncation warning");
403    }
404
405    // ── Range expression tests (P1) ────────────────────────────────────
406
407    #[test]
408    fn test_range_field_to_end() {
409        // @crc32(@self[magic..]) — from the 'magic' field to end of struct
410        let dsl = r#"
411            @endian = little;
412            struct header @packed {
413                magic:  [u8; 4] = @bytes("TEST");
414                crc:    u32     = @crc32(@self[magic..]);
415            }
416        "#;
417        let env = HashMap::new();
418        let sections = HashMap::new();
419        let result = generate(dsl, &env, &sections).unwrap();
420        assert_eq!(result.data.len(), 8);
421        // Verify CRC is non-zero and matches manual calculation
422        let crc_bytes = &result.data[4..8];
423        assert_ne!(crc_bytes, &[0u8; 4], "CRC should not be zero");
424    }
425
426    #[test]
427    fn test_range_field_to_field() {
428        // @crc32(@self[magic..body_crc]) — two-field range
429        let dsl = r#"
430            @endian = little;
431            struct header @packed {
432                magic:    [u8; 4] = @bytes("TEST");
433                reserved: u32     = 0;
434                body_crc: u32     = @crc32(@self[magic..body_crc]);
435            }
436        "#;
437        let env = HashMap::new();
438        let sections = HashMap::new();
439        let result = generate(dsl, &env, &sections).unwrap();
440        assert_eq!(result.data.len(), 12);
441        let crc_bytes = &result.data[8..12];
442        assert_ne!(crc_bytes, &[0u8; 4], "CRC should not be zero");
443    }
444
445    // ── P1: env var / shift overflow / @crc unified ────────────────────
446
447    #[test]
448    fn test_undefined_env_var_is_error() {
449        let dsl = r#"
450            @endian = little;
451            struct header @packed {
452                ver: u8 = ${MISSING_VAR};
453            }
454        "#;
455        let result = generate(dsl, &HashMap::new(), &HashMap::new());
456        assert!(result.is_err(), "expected Err for undefined env var");
457        assert_eq!(result.unwrap_err().code, ErrorCode::E02001);
458    }
459
460    #[test]
461    fn test_shift_by_64_emits_warning_and_returns_zero() {
462        // 1 << 64 cannot fit in u64; should warn W04001 and produce 0
463        let dsl = r#"
464            @endian = little;
465            struct header @packed {
466                val: u64 = 1 << 64;
467            }
468        "#;
469        let result = generate(dsl, &HashMap::new(), &HashMap::new()).unwrap();
470        assert_eq!(result.data, vec![0u8; 8], "result should be 0 when shift >= 64");
471        assert!(
472            result.warnings.iter().any(|w| w.code == WarningCode::W04001),
473            "expected W04001 ShiftOverflow warning"
474        );
475    }
476
477    #[test]
478    fn test_crc_unified_equals_crc32() {
479        // @crc("crc32", @self[..]) should produce the same bytes as @crc32(@self[..])
480        let env = HashMap::new();
481        let sects = HashMap::new();
482
483        let dsl_unified = r#"
484            @endian = little;
485            struct header @packed {
486                magic: [u8; 4] = @bytes("TEST");
487                crc:   u32     = @crc("crc32", @self[magic..crc]);
488            }
489        "#;
490        let dsl_legacy = r#"
491            @endian = little;
492            struct header @packed {
493                magic: [u8; 4] = @bytes("TEST");
494                crc:   u32     = @crc32(@self[magic..crc]);
495            }
496        "#;
497
498        let unified = generate(dsl_unified, &env, &sects).unwrap();
499        let legacy  = generate(dsl_legacy,  &env, &sects).unwrap();
500        assert_eq!(unified.data, legacy.data, "@crc(\"crc32\",...) must equal @crc32(...)");
501    }
502
503    #[test]
504    fn test_crc_unified_crc16_modbus() {
505        let mut sections = HashMap::new();
506        sections.insert("fw".to_string(), vec![0x01u8, 0x02, 0x03, 0x04]);
507
508        let dsl = r#"
509            @endian = little;
510            struct header @packed {
511                crc16: u16 = @crc("crc16-modbus", fw);
512            }
513        "#;
514        let result = generate(dsl, &HashMap::new(), &sections).unwrap();
515        assert_eq!(result.data.len(), 2);
516        let crc = u16::from_le_bytes([result.data[0], result.data[1]]);
517        assert_ne!(crc, 0, "CRC16-MODBUS should not be zero for non-empty input");
518    }
519
520    #[test]
521    fn test_crc_unknown_algorithm_is_error() {
522        let mut sections = HashMap::new();
523        sections.insert("fw".to_string(), vec![0xAAu8]);
524
525        let dsl = r#"
526            @endian = little;
527            struct header @packed {
528                crc: u32 = @crc("nonexistent-algo", fw);
529            }
530        "#;
531        let result = generate(dsl, &HashMap::new(), &sections);
532        assert!(result.is_err(), "unknown CRC algorithm should return Err");
533        assert_eq!(result.unwrap_err().code, ErrorCode::E04003);
534    }
535
536    // ── P2: @align(n) padding ───────────────────────────────────────────
537
538    #[test]
539    fn test_align_4_pads_to_boundary() {
540        // u8(1) + u16(2) = 3 bytes raw → padded to 4 with @align(4)
541        let dsl = r#"
542            @endian = little;
543            struct header @align(4) {
544                tag: u8  = 0xAB;
545                val: u16 = 0x1234;
546            }
547        "#;
548        let result = generate(dsl, &HashMap::new(), &HashMap::new()).unwrap();
549        assert_eq!(result.data.len(), 4, "aligned struct should be 4 bytes");
550        assert_eq!(result.data[0], 0xAB);
551        assert_eq!(result.data[1], 0x34); // little-endian low byte
552        assert_eq!(result.data[2], 0x12); // little-endian high byte
553        assert_eq!(result.data[3], 0x00); // padding
554    }
555
556    #[test]
557    fn test_align_already_aligned_no_extra_padding() {
558        // u32(4) = 4 bytes raw → already aligned to 4, no padding
559        let dsl = r#"
560            @endian = little;
561            struct header @align(4) {
562                val: u32 = 0xDEADBEEF;
563            }
564        "#;
565        let result = generate(dsl, &HashMap::new(), &HashMap::new()).unwrap();
566        assert_eq!(result.data.len(), 4);
567    }
568
569    // ── P3: validate() API ─────────────────────────────────────────────
570
571    #[test]
572    fn test_validate_valid_dsl_returns_ok() {
573        let dsl = r#"
574            @endian = little;
575            struct header @packed {
576                version: u8 = 1;
577            }
578        "#;
579        let result = validate(dsl, &HashMap::new());
580        assert!(result.is_ok(), "valid DSL should pass validate()");
581    }
582
583    #[test]
584    fn test_validate_invalid_syntax_returns_error() {
585        let result = validate("this is not valid dsl", &HashMap::new());
586        assert!(result.is_err(), "invalid syntax should fail validate()");
587    }
588
589    #[test]
590    fn test_validate_undefined_env_var_returns_error() {
591        let dsl = r#"
592            @endian = little;
593            struct header @packed {
594                ver: u8 = ${NO_SUCH_VAR};
595            }
596        "#;
597        let result = validate(dsl, &HashMap::new());
598        assert!(result.is_err(), "undefined env var should fail validate()");
599        assert_eq!(result.unwrap_err().code, ErrorCode::E02001);
600    }
601
602    #[test]
603    fn test_validate_returns_warnings_for_truncation() {
604        let dsl = r#"
605            @endian = little;
606            struct header @packed {
607                small: u8 = 0x1FF;
608            }
609        "#;
610        let warnings = validate(dsl, &HashMap::new()).unwrap();
611        assert!(!warnings.is_empty(), "truncation should produce a warning");
612        assert!(warnings.iter().any(|w| w.code == WarningCode::W03002));
613    }
614
615    // ── P3: parse() API ────────────────────────────────────────────────
616
617    #[test]
618    fn test_parse_scalar_fields_little_endian() {
619        let dsl = "@endian = little; struct h @packed { ver: u8; flags: u16; size: u32; }";
620        let data: &[u8] = &[0x01, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12];
621        let result = parse(dsl, &HashMap::new(), data).unwrap();
622        assert_eq!(result["ver"].as_u64().unwrap(), 0x01);
623        assert_eq!(result["flags"].as_u64().unwrap(), 0x1234);
624        assert_eq!(result["size"].as_u64().unwrap(), 0x12345678);
625    }
626
627    #[test]
628    fn test_parse_scalar_fields_big_endian() {
629        let dsl = "@endian = big; struct h @packed { val: u32; }";
630        let data: &[u8] = &[0x12, 0x34, 0x56, 0x78];
631        let result = parse(dsl, &HashMap::new(), data).unwrap();
632        assert_eq!(result["val"].as_u64().unwrap(), 0x12345678);
633    }
634
635    #[test]
636    fn test_parse_array_field_returns_bytes() {
637        let dsl = "@endian = little; struct h @packed { magic: [u8; 4]; }";
638        let data: &[u8] = b"TEST";
639        let result = parse(dsl, &HashMap::new(), data).unwrap();
640        assert_eq!(result["magic"].as_bytes().unwrap(), b"TEST");
641    }
642
643    #[test]
644    fn test_parse_data_too_short_is_error() {
645        let dsl = "@endian = little; struct h @packed { size: u32; }";
646        let data: &[u8] = &[0x01, 0x02]; // only 2 bytes, needs 4
647        let result = parse(dsl, &HashMap::new(), data);
648        assert!(result.is_err(), "short data should return Err");
649    }
650
651    #[test]
652    fn test_parse_roundtrip() {
653        let dsl = r#"
654            @endian = little;
655            struct h @packed {
656                version: u8  = 3;
657                flags:   u16 = 0x1234;
658                size:    u32 = 0xDEADBEEF;
659            }
660        "#;
661        let generated = generate(dsl, &HashMap::new(), &HashMap::new()).unwrap();
662        let parsed = parse(dsl, &HashMap::new(), &generated.data).unwrap();
663        assert_eq!(parsed["version"].as_u64().unwrap(), 3);
664        assert_eq!(parsed["flags"].as_u64().unwrap(), 0x1234);
665        assert_eq!(parsed["size"].as_u64().unwrap(), 0xDEAD_BEEF);
666    }
667}