⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
reproducible.rs

1//! Reproducible Builds Infrastructure
2//!
3//! Ensures deterministic build outputs by normalizing build environments,
4//! recording build inputs/outputs, and verifying reproducibility across
5//! independent builds.
6
7#[cfg(feature = "alloc")]
8extern crate alloc;
9
10#[cfg(feature = "alloc")]
11use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
12
13#[cfg(feature = "alloc")]
14use crate::error::KernelError;
15
16// ---------------------------------------------------------------------------
17// Types
18// ---------------------------------------------------------------------------
19
20/// Captures the complete build environment state for reproducibility.
21#[cfg(feature = "alloc")]
22#[derive(Debug, Clone)]
23pub struct BuildSnapshot {
24    /// Compiler/toolchain version string (e.g., "rustc 1.93.0-nightly")
25    pub toolchain_version: String,
26    /// Sorted environment variables present at build time
27    pub env_vars: BTreeMap<String, String>,
28    /// If set, replaces real timestamps for reproducibility
29    pub timestamp_override: Option<u64>,
30    /// (filename, SHA-256 hash) pairs for source files
31    pub source_hashes: Vec<(String, [u8; 32])>,
32    /// Target triple (e.g., "x86_64-veridian")
33    pub target_triple: String,
34}
35
36#[cfg(feature = "alloc")]
37impl BuildSnapshot {
38    /// Create a new empty snapshot.
39    pub fn new() -> Self {
40        Self {
41            toolchain_version: String::new(),
42            env_vars: BTreeMap::new(),
43            timestamp_override: None,
44            source_hashes: Vec::new(),
45            target_triple: String::new(),
46        }
47    }
48}
49
50#[cfg(feature = "alloc")]
51impl Default for BuildSnapshot {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57/// Records the complete inputs and outputs of a single build.
58#[cfg(feature = "alloc")]
59#[derive(Debug, Clone)]
60pub struct BuildManifest {
61    /// Port name that was built
62    pub port_name: String,
63    /// Port version that was built
64    pub port_version: String,
65    /// All build inputs
66    pub inputs: BuildInputs,
67    /// All build outputs
68    pub outputs: BuildOutputs,
69    /// Wall-clock build duration in milliseconds
70    pub build_duration_ms: u64,
71}
72
73/// Input specification for a build.
74#[cfg(feature = "alloc")]
75#[derive(Debug, Clone)]
76pub struct BuildInputs {
77    /// SHA-256 hashes of source files
78    pub source_hashes: Vec<(String, [u8; 32])>,
79    /// Environment snapshot at build time
80    pub env_snapshot: BuildSnapshot,
81    /// Dependency name -> version mapping
82    pub dependency_versions: BTreeMap<String, String>,
83}
84
85#[cfg(feature = "alloc")]
86impl BuildInputs {
87    /// Create empty build inputs.
88    pub fn new() -> Self {
89        Self {
90            source_hashes: Vec::new(),
91            env_snapshot: BuildSnapshot::new(),
92            dependency_versions: BTreeMap::new(),
93        }
94    }
95}
96
97#[cfg(feature = "alloc")]
98impl Default for BuildInputs {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// Output specification for a build.
105#[cfg(feature = "alloc")]
106#[derive(Debug, Clone)]
107pub struct BuildOutputs {
108    /// (filename, SHA-256 hash) pairs for output files
109    pub file_hashes: Vec<(String, [u8; 32])>,
110    /// Total output size in bytes
111    pub total_size: u64,
112    /// Number of output files
113    pub file_count: usize,
114}
115
116#[cfg(feature = "alloc")]
117impl BuildOutputs {
118    /// Create empty build outputs.
119    pub fn new() -> Self {
120        Self {
121            file_hashes: Vec::new(),
122            total_size: 0,
123            file_count: 0,
124        }
125    }
126}
127
128#[cfg(feature = "alloc")]
129impl Default for BuildOutputs {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135/// Result of comparing two build manifests for reproducibility.
136#[cfg(feature = "alloc")]
137#[derive(Debug, Clone)]
138pub struct ReproducibilityResult {
139    /// Files whose hashes match between both builds
140    pub matching_files: Vec<String>,
141    /// Files present in both builds with differing hashes: (path, hash_a,
142    /// hash_b)
143    pub differing_files: Vec<(String, [u8; 32], [u8; 32])>,
144    /// Files present only in build B (missing from A)
145    pub missing_in_a: Vec<String>,
146    /// Files present only in build A (missing from B)
147    pub missing_in_b: Vec<String>,
148}
149
150#[cfg(feature = "alloc")]
151impl ReproducibilityResult {
152    /// Returns true if the builds are fully reproducible (all outputs match).
153    pub fn is_reproducible(&self) -> bool {
154        self.differing_files.is_empty()
155            && self.missing_in_a.is_empty()
156            && self.missing_in_b.is_empty()
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Functions
162// ---------------------------------------------------------------------------
163
164/// Normalize a build environment for reproducibility.
165///
166/// - Sets `SOURCE_DATE_EPOCH` to "0" to eliminate timestamp variation
167/// - Removes locale-dependent variables (`LC_ALL`, `LANG`, `LANGUAGE`) and sets
168///   them to "C" for deterministic collation
169/// - Sets `TZ=UTC` for timezone consistency
170/// - Canonicalizes paths by stripping trailing slashes and collapsing double
171///   slashes
172#[cfg(feature = "alloc")]
173pub fn normalize_environment(env: &mut super::ports::BuildEnvironment) {
174    // Zero out timestamp-dependent variables
175    env.env_vars
176        .insert(String::from("SOURCE_DATE_EPOCH"), String::from("0"));
177
178    // Set locale to "C" for deterministic collation/formatting
179    env.env_vars
180        .insert(String::from("LC_ALL"), String::from("C"));
181    env.env_vars.insert(String::from("LANG"), String::from("C"));
182    env.env_vars
183        .insert(String::from("LANGUAGE"), String::from("C"));
184
185    // Set timezone to UTC
186    env.env_vars.insert(String::from("TZ"), String::from("UTC"));
187
188    // Canonicalize path values: strip trailing slashes, collapse double slashes
189    let keys: Vec<String> = env.env_vars.keys().cloned().collect();
190    for key in keys {
191        if let Some(val) = env.env_vars.get_mut(&key) {
192            *val = canonicalize_path_value(val);
193        }
194    }
195}
196
197/// Canonicalize a path string: collapse double slashes and strip trailing
198/// slash.
199#[cfg(feature = "alloc")]
200fn canonicalize_path_value(path: &str) -> String {
201    let mut result = String::with_capacity(path.len());
202    let mut prev_slash = false;
203
204    for ch in path.chars() {
205        if ch == '/' {
206            if !prev_slash {
207                result.push('/');
208            }
209            prev_slash = true;
210        } else {
211            prev_slash = false;
212            result.push(ch);
213        }
214    }
215
216    // Strip trailing slash unless the path is exactly "/"
217    if result.len() > 1 && result.ends_with('/') {
218        result.pop();
219    }
220
221    result
222}
223
224/// Create a build manifest recording all inputs and outputs.
225///
226/// Reads the port's source checksums as inputs and attempts to walk
227/// `output_dir` via the VFS to compute output file hashes. If the VFS
228/// is unavailable, outputs are recorded as empty.
229#[cfg(feature = "alloc")]
230pub fn create_build_manifest(
231    port: &super::ports::Port,
232    env: &super::ports::BuildEnvironment,
233    output_dir: &str,
234) -> Result<BuildManifest, KernelError> {
235    // Build inputs from port checksums
236    let mut source_hashes = Vec::new();
237    for (i, checksum) in port.checksums.iter().enumerate() {
238        let name = if i < port.sources.len() {
239            port.sources[i].clone()
240        } else {
241            alloc::format!("source-{}", i)
242        };
243        source_hashes.push((name, *checksum));
244    }
245
246    // Build environment snapshot
247    let env_snapshot = BuildSnapshot {
248        toolchain_version: env
249            .get_env("RUSTC_VERSION")
250            .map(String::from)
251            .unwrap_or_default(),
252        env_vars: env.env_vars.clone(),
253        timestamp_override: Some(0), // Reproducible builds always override
254        source_hashes: source_hashes.clone(),
255        target_triple: env.get_env("TARGET").map(String::from).unwrap_or_default(),
256    };
257
258    // Dependency versions from environment
259    let mut dependency_versions = BTreeMap::new();
260    for dep in &port.dependencies {
261        dependency_versions.insert(dep.clone(), String::from("*"));
262    }
263
264    let inputs = BuildInputs {
265        source_hashes,
266        env_snapshot,
267        dependency_versions,
268    };
269
270    // Compute outputs by walking the output directory via VFS
271    let outputs = compute_outputs(output_dir);
272
273    Ok(BuildManifest {
274        port_name: port.name.clone(),
275        port_version: port.version.clone(),
276        inputs,
277        outputs,
278        build_duration_ms: 0, // Actual timing comes from the build runner
279    })
280}
281
282/// Walk an output directory via VFS and hash all files found.
283///
284/// Returns empty outputs if the VFS is unavailable or the path does
285/// not exist.
286#[cfg(feature = "alloc")]
287fn compute_outputs(output_dir: &str) -> BuildOutputs {
288    let mut outputs = BuildOutputs::new();
289
290    let vfs_lock = match crate::fs::try_get_vfs() {
291        Some(lock) => lock,
292        None => {
293            crate::println!(
294                "[REPRO] VFS unavailable, recording empty outputs for {}",
295                output_dir
296            );
297            return outputs;
298        }
299    };
300
301    let vfs = vfs_lock.read();
302
303    // Try to resolve the output directory
304    let dir_node = match vfs.resolve_path(output_dir) {
305        Ok(node) => node,
306        Err(_) => {
307            crate::println!(
308                "[REPRO] Output directory not found: {}, recording empty outputs",
309                output_dir
310            );
311            return outputs;
312        }
313    };
314
315    // Read directory entries
316    let entries = match dir_node.readdir() {
317        Ok(entries) => entries,
318        Err(_) => {
319            return outputs;
320        }
321    };
322
323    for entry in &entries {
324        // Skip . and .. entries
325        if entry.name == "." || entry.name == ".." {
326            continue;
327        }
328
329        if entry.node_type == crate::fs::NodeType::File {
330            let file_path = alloc::format!("{}/{}", output_dir, entry.name);
331            if let Ok(node) = vfs.resolve_path(&file_path) {
332                if let Ok(metadata) = node.metadata() {
333                    let size = metadata.size;
334                    // Read file contents to hash
335                    let mut buf = vec![0u8; size];
336                    if let Ok(bytes_read) = node.read(0, &mut buf) {
337                        buf.truncate(bytes_read);
338                        let hash = crate::crypto::hash::sha256(&buf);
339                        outputs
340                            .file_hashes
341                            .push((entry.name.clone(), *hash.as_bytes()));
342                        outputs.total_size += size as u64;
343                        outputs.file_count += 1;
344                    }
345                }
346            }
347        }
348    }
349
350    outputs
351}
352
353/// Compare two build manifests and produce a reproducibility report.
354///
355/// Walks through all output files in both manifests, categorizing each
356/// file as matching, differing, or missing from one side.
357#[cfg(feature = "alloc")]
358pub fn verify_reproducible(a: &BuildManifest, b: &BuildManifest) -> ReproducibilityResult {
359    let map_a: BTreeMap<&str, &[u8; 32]> = a
360        .outputs
361        .file_hashes
362        .iter()
363        .map(|(name, hash)| (name.as_str(), hash))
364        .collect();
365
366    let map_b: BTreeMap<&str, &[u8; 32]> = b
367        .outputs
368        .file_hashes
369        .iter()
370        .map(|(name, hash)| (name.as_str(), hash))
371        .collect();
372
373    let mut matching_files = Vec::new();
374    let mut differing_files = Vec::new();
375    let mut missing_in_a = Vec::new();
376    let mut missing_in_b = Vec::new();
377
378    // Check all files in A
379    for (name, hash_a) in &map_a {
380        match map_b.get(name) {
381            Some(hash_b) => {
382                if hash_a == hash_b {
383                    matching_files.push(String::from(*name));
384                } else {
385                    differing_files.push((String::from(*name), **hash_a, **hash_b));
386                }
387            }
388            None => {
389                missing_in_b.push(String::from(*name));
390            }
391        }
392    }
393
394    // Check for files only in B
395    for name in map_b.keys() {
396        if !map_a.contains_key(name) {
397            missing_in_a.push(String::from(*name));
398        }
399    }
400
401    ReproducibilityResult {
402        matching_files,
403        differing_files,
404        missing_in_a,
405        missing_in_b,
406    }
407}
408
409/// Serialize a build manifest to a simple text format for VFS storage.
410///
411/// Format:
412/// ```text
413/// PORT={name}
414/// VERSION={version}
415/// TOOLCHAIN={toolchain_version}
416/// TARGET={target_triple}
417/// DURATION_MS={duration}
418/// INPUT_COUNT={count}
419/// INPUT:{filename}={hex_hash}
420/// ...
421/// OUTPUT_COUNT={count}
422/// OUTPUT:{filename}={hex_hash}
423/// ...
424/// TOTAL_SIZE={size}
425/// ```
426#[cfg(feature = "alloc")]
427pub fn serialize_manifest(manifest: &BuildManifest) -> Vec<u8> {
428    let mut out = String::new();
429
430    out.push_str("PORT=");
431    out.push_str(&manifest.port_name);
432    out.push('\n');
433
434    out.push_str("VERSION=");
435    out.push_str(&manifest.port_version);
436    out.push('\n');
437
438    out.push_str("TOOLCHAIN=");
439    out.push_str(&manifest.inputs.env_snapshot.toolchain_version);
440    out.push('\n');
441
442    out.push_str("TARGET=");
443    out.push_str(&manifest.inputs.env_snapshot.target_triple);
444    out.push('\n');
445
446    out.push_str("DURATION_MS=");
447    push_u64(&mut out, manifest.build_duration_ms);
448    out.push('\n');
449
450    // Input hashes
451    out.push_str("INPUT_COUNT=");
452    push_usize(&mut out, manifest.inputs.source_hashes.len());
453    out.push('\n');
454
455    for (name, hash) in &manifest.inputs.source_hashes {
456        out.push_str("INPUT:");
457        out.push_str(name);
458        out.push('=');
459        out.push_str(&bytes_to_hex(hash));
460        out.push('\n');
461    }
462
463    // Output hashes
464    out.push_str("OUTPUT_COUNT=");
465    push_usize(&mut out, manifest.outputs.file_hashes.len());
466    out.push('\n');
467
468    for (name, hash) in &manifest.outputs.file_hashes {
469        out.push_str("OUTPUT:");
470        out.push_str(name);
471        out.push('=');
472        out.push_str(&bytes_to_hex(hash));
473        out.push('\n');
474    }
475
476    out.push_str("TOTAL_SIZE=");
477    push_u64(&mut out, manifest.outputs.total_size);
478    out.push('\n');
479
480    out.into_bytes()
481}
482
483/// Convert a byte slice to a lowercase hex string.
484#[cfg(feature = "alloc")]
485fn bytes_to_hex(bytes: &[u8]) -> String {
486    let mut hex = String::with_capacity(bytes.len() * 2);
487    for &b in bytes {
488        let high = HEX_CHARS[(b >> 4) as usize];
489        let low = HEX_CHARS[(b & 0x0f) as usize];
490        hex.push(high as char);
491        hex.push(low as char);
492    }
493    hex
494}
495
496/// Hex character lookup table.
497#[cfg(feature = "alloc")]
498const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
499
500/// Append a `u64` as decimal to a string (no_std helper).
501#[cfg(feature = "alloc")]
502fn push_u64(s: &mut String, value: u64) {
503    use core::fmt::Write;
504    let _ = write!(s, "{}", value);
505}
506
507/// Append a `usize` as decimal to a string (no_std helper).
508#[cfg(feature = "alloc")]
509fn push_usize(s: &mut String, value: usize) {
510    use core::fmt::Write;
511    let _ = write!(s, "{}", value);
512}
513
514#[cfg(test)]
515mod tests {
516    #[allow(unused_imports)]
517    use alloc::vec;
518
519    use super::*;
520
521    // ---- BuildSnapshot ----
522
523    #[test]
524    fn test_build_snapshot_new() {
525        let snap = BuildSnapshot::new();
526        assert!(snap.toolchain_version.is_empty());
527        assert!(snap.env_vars.is_empty());
528        assert!(snap.timestamp_override.is_none());
529        assert!(snap.source_hashes.is_empty());
530        assert!(snap.target_triple.is_empty());
531    }
532
533    // ---- BuildInputs / BuildOutputs ----
534
535    #[test]
536    fn test_build_inputs_new() {
537        let inp = BuildInputs::new();
538        assert!(inp.source_hashes.is_empty());
539        assert!(inp.dependency_versions.is_empty());
540    }
541
542    #[test]
543    fn test_build_outputs_new() {
544        let out = BuildOutputs::new();
545        assert!(out.file_hashes.is_empty());
546        assert_eq!(out.total_size, 0);
547        assert_eq!(out.file_count, 0);
548    }
549
550    // ---- ReproducibilityResult ----
551
552    #[test]
553    fn test_reproducibility_result_is_reproducible() {
554        let result = ReproducibilityResult {
555            matching_files: vec![String::from("a.o")],
556            differing_files: vec![],
557            missing_in_a: vec![],
558            missing_in_b: vec![],
559        };
560        assert!(result.is_reproducible());
561    }
562
563    #[test]
564    fn test_reproducibility_result_not_reproducible_differing() {
565        let result = ReproducibilityResult {
566            matching_files: vec![],
567            differing_files: vec![(String::from("a.o"), [1u8; 32], [2u8; 32])],
568            missing_in_a: vec![],
569            missing_in_b: vec![],
570        };
571        assert!(!result.is_reproducible());
572    }
573
574    #[test]
575    fn test_reproducibility_result_not_reproducible_missing() {
576        let result = ReproducibilityResult {
577            matching_files: vec![],
578            differing_files: vec![],
579            missing_in_a: vec![String::from("b.o")],
580            missing_in_b: vec![],
581        };
582        assert!(!result.is_reproducible());
583    }
584
585    // ---- canonicalize_path_value ----
586
587    #[test]
588    fn test_canonicalize_path_no_change() {
589        assert_eq!(canonicalize_path_value("/usr/bin"), "/usr/bin");
590    }
591
592    #[test]
593    fn test_canonicalize_path_trailing_slash() {
594        assert_eq!(canonicalize_path_value("/usr/bin/"), "/usr/bin");
595    }
596
597    #[test]
598    fn test_canonicalize_path_double_slash() {
599        assert_eq!(canonicalize_path_value("/usr//bin"), "/usr/bin");
600    }
601
602    #[test]
603    fn test_canonicalize_path_root_only() {
604        assert_eq!(canonicalize_path_value("/"), "/");
605    }
606
607    #[test]
608    fn test_canonicalize_path_non_path() {
609        assert_eq!(canonicalize_path_value("hello"), "hello");
610    }
611
612    // ---- normalize_environment ----
613
614    #[test]
615    fn test_normalize_environment() {
616        use super::super::ports::{BuildEnvironment, Port};
617        let port = Port::new(String::from("test"), String::from("1.0.0"));
618        let mut env = BuildEnvironment::new(&port);
619        normalize_environment(&mut env);
620        assert_eq!(env.get_env("SOURCE_DATE_EPOCH"), Some("0"));
621        assert_eq!(env.get_env("LC_ALL"), Some("C"));
622        assert_eq!(env.get_env("LANG"), Some("C"));
623        assert_eq!(env.get_env("LANGUAGE"), Some("C"));
624        assert_eq!(env.get_env("TZ"), Some("UTC"));
625    }
626
627    // ---- bytes_to_hex ----
628
629    #[test]
630    fn test_bytes_to_hex() {
631        assert_eq!(bytes_to_hex(&[0x00, 0xff, 0xab]), "00ffab");
632        assert_eq!(bytes_to_hex(&[]), "");
633        assert_eq!(bytes_to_hex(&[0x01, 0x23, 0x45, 0x67]), "01234567");
634    }
635
636    // ---- verify_reproducible ----
637
638    #[test]
639    fn test_verify_reproducible_identical() {
640        let a = BuildManifest {
641            port_name: String::from("test"),
642            port_version: String::from("1.0.0"),
643            inputs: BuildInputs::new(),
644            outputs: BuildOutputs {
645                file_hashes: vec![(String::from("a.o"), [1u8; 32])],
646                total_size: 100,
647                file_count: 1,
648            },
649            build_duration_ms: 0,
650        };
651        let b = a.clone();
652        let result = verify_reproducible(&a, &b);
653        assert!(result.is_reproducible());
654        assert_eq!(result.matching_files.len(), 1);
655    }
656
657    #[test]
658    fn test_verify_reproducible_differing() {
659        let a = BuildManifest {
660            port_name: String::from("test"),
661            port_version: String::from("1.0.0"),
662            inputs: BuildInputs::new(),
663            outputs: BuildOutputs {
664                file_hashes: vec![(String::from("a.o"), [1u8; 32])],
665                total_size: 100,
666                file_count: 1,
667            },
668            build_duration_ms: 0,
669        };
670        let b = BuildManifest {
671            port_name: String::from("test"),
672            port_version: String::from("1.0.0"),
673            inputs: BuildInputs::new(),
674            outputs: BuildOutputs {
675                file_hashes: vec![(String::from("a.o"), [2u8; 32])],
676                total_size: 100,
677                file_count: 1,
678            },
679            build_duration_ms: 0,
680        };
681        let result = verify_reproducible(&a, &b);
682        assert!(!result.is_reproducible());
683        assert_eq!(result.differing_files.len(), 1);
684    }
685
686    #[test]
687    fn test_verify_reproducible_missing_files() {
688        let a = BuildManifest {
689            port_name: String::from("test"),
690            port_version: String::from("1.0.0"),
691            inputs: BuildInputs::new(),
692            outputs: BuildOutputs {
693                file_hashes: vec![(String::from("a.o"), [1u8; 32])],
694                total_size: 100,
695                file_count: 1,
696            },
697            build_duration_ms: 0,
698        };
699        let b = BuildManifest {
700            port_name: String::from("test"),
701            port_version: String::from("1.0.0"),
702            inputs: BuildInputs::new(),
703            outputs: BuildOutputs {
704                file_hashes: vec![(String::from("b.o"), [1u8; 32])],
705                total_size: 100,
706                file_count: 1,
707            },
708            build_duration_ms: 0,
709        };
710        let result = verify_reproducible(&a, &b);
711        assert!(!result.is_reproducible());
712        assert_eq!(result.missing_in_b.len(), 1); // a.o missing in b
713        assert_eq!(result.missing_in_a.len(), 1); // b.o missing in a
714    }
715
716    // ---- serialize_manifest ----
717
718    #[test]
719    fn test_serialize_manifest() {
720        let manifest = BuildManifest {
721            port_name: String::from("curl"),
722            port_version: String::from("8.5.0"),
723            inputs: BuildInputs {
724                source_hashes: vec![(String::from("main.c"), [0xABu8; 32])],
725                env_snapshot: BuildSnapshot {
726                    toolchain_version: String::from("rustc 1.93"),
727                    env_vars: BTreeMap::new(),
728                    timestamp_override: Some(0),
729                    source_hashes: vec![],
730                    target_triple: String::from("x86_64-veridian"),
731                },
732                dependency_versions: BTreeMap::new(),
733            },
734            outputs: BuildOutputs {
735                file_hashes: vec![(String::from("curl"), [0xCDu8; 32])],
736                total_size: 12345,
737                file_count: 1,
738            },
739            build_duration_ms: 5000,
740        };
741        let bytes = serialize_manifest(&manifest);
742        let text = core::str::from_utf8(&bytes).unwrap();
743        assert!(text.contains("PORT=curl"));
744        assert!(text.contains("VERSION=8.5.0"));
745        assert!(text.contains("TOOLCHAIN=rustc 1.93"));
746        assert!(text.contains("TARGET=x86_64-veridian"));
747        assert!(text.contains("DURATION_MS=5000"));
748        assert!(text.contains("INPUT_COUNT=1"));
749        assert!(text.contains("OUTPUT_COUNT=1"));
750        assert!(text.contains("TOTAL_SIZE=12345"));
751        assert!(text.contains("INPUT:main.c="));
752        assert!(text.contains("OUTPUT:curl="));
753    }
754
755    // ---- push_u64, push_usize ----
756
757    #[test]
758    fn test_push_u64() {
759        let mut s = String::new();
760        push_u64(&mut s, 42);
761        assert_eq!(s, "42");
762    }
763
764    #[test]
765    fn test_push_usize() {
766        let mut s = String::new();
767        push_usize(&mut s, 0);
768        assert_eq!(s, "0");
769    }
770}