fuel_etk_asm/
ingest.rs

1//! High-level interface for assembling instructions.
2//!
3//! See the [`Ingest`] documentation for examples and more information.
4mod error {
5    use crate::asm::Error as AssembleError;
6    use crate::ParseError;
7
8    use snafu::{Backtrace, Snafu};
9
10    use std::path::PathBuf;
11
12    /// Errors that may arise during the assembly process.
13    #[derive(Debug, Snafu)]
14    #[non_exhaustive]
15    #[snafu(context(suffix(false)), visibility(pub(super)))]
16    pub enum Error {
17        /// An included/imported file was outside of the root directory.
18        #[snafu(display(
19            "`{}` is outside of the root directory `{}`",
20            file.display(),
21            root.display()
22        ))]
23        #[non_exhaustive]
24        DirectoryTraversal {
25            /// The root directory.
26            root: PathBuf,
27
28            /// The file that was to be included or imported.
29            file: PathBuf,
30        },
31
32        /// An i/o error.
33        #[snafu(display(
34            "an i/o error occurred on path `{}` ({})",
35            path.as_ref().map(|p| p.display().to_string()).unwrap_or_default(),
36            message,
37        ))]
38        #[non_exhaustive]
39        Io {
40            /// The underlying source of this error.
41            source: std::io::Error,
42
43            /// Extra information about the i/o error.
44            message: String,
45
46            /// The location of the error.
47            backtrace: Backtrace,
48
49            /// The optional path where the error occurred.
50            path: Option<PathBuf>,
51        },
52
53        /// An error that occurred while parsing a file.
54        #[snafu(context(false))]
55        #[non_exhaustive]
56        #[snafu(display("parsing failed"))]
57        Parse {
58            /// The underlying source of this error.
59            #[snafu(backtrace)]
60            source: ParseError,
61        },
62
63        /// An error that occurred while assembling a file.
64        #[snafu(context(false))]
65        #[non_exhaustive]
66        #[snafu(display("assembling failed"))]
67        Assemble {
68            /// The underlying source of this error.
69            #[snafu(backtrace)]
70            source: AssembleError,
71        },
72
73        /// An included fail failed to parse as hexadecimal.
74        #[snafu(display("included file `{}` is invalid hex: {}", path.to_string_lossy(), source))]
75        #[non_exhaustive]
76        InvalidHex {
77            /// Path to the offending file.
78            path: PathBuf,
79
80            /// The underlying source of this error.
81            source: Box<dyn std::error::Error>,
82
83            /// The location of the error.
84            backtrace: Backtrace,
85        },
86
87        /// A recursion limit was reached while including or importing a file.
88        #[snafu(display("too many levels of recursion/includes"))]
89        #[non_exhaustive]
90        RecursionLimit {
91            /// The location of the error.
92            backtrace: Backtrace,
93        },
94    }
95}
96
97use crate::asm::{Assembler, RawOp};
98use crate::ast::Node;
99use crate::parse::parse_asm;
100
101pub use self::error::Error;
102
103use snafu::{ensure, ResultExt};
104
105use std::fs::{read_to_string, File};
106use std::io::{self, Read, Write};
107use std::path::{Path, PathBuf};
108
109fn parse_file<P: AsRef<Path>>(path: P) -> Result<Vec<Node>, Error> {
110    let asm = read_to_string(path.as_ref()).with_context(|_| error::Io {
111        message: "reading file before parsing",
112        path: path.as_ref().to_owned(),
113    })?;
114    let nodes = parse_asm(&asm)?;
115
116    Ok(nodes)
117}
118
119#[derive(Debug)]
120enum Scope {
121    Same,
122    Independent(Box<Assembler>),
123}
124
125impl Scope {
126    fn same() -> Self {
127        Self::Same
128    }
129
130    fn independent() -> Self {
131        Self::Independent(Box::new(Assembler::new()))
132    }
133}
134
135#[derive(Debug)]
136struct Source {
137    path: PathBuf,
138    nodes: std::vec::IntoIter<Node>,
139    scope: Scope,
140}
141
142#[derive(Debug)]
143struct Root {
144    original: PathBuf,
145    canonicalized: PathBuf,
146}
147
148impl Root {
149    fn new(mut file: PathBuf) -> Result<Self, Error> {
150        // Pop the filename.
151        if !file.pop() {
152            return Err(io::Error::from(io::ErrorKind::NotFound)).context(error::Io {
153                message: "no parent",
154                path: Some(file),
155            });
156        }
157
158        let file = std::env::current_dir()
159            .context(error::Io {
160                message: "getting cwd",
161                path: None,
162            })?
163            .join(file);
164
165        let metadata = file.metadata().with_context(|_| error::Io {
166            message: "getting metadata",
167            path: file.clone(),
168        })?;
169
170        // Root must be a directory.
171        if !metadata.is_dir() {
172            let err = io::Error::from(io::ErrorKind::NotFound);
173            return Err(err).context(error::Io {
174                message: "root is not directory",
175                path: file,
176            });
177        }
178
179        let canonicalized = std::fs::canonicalize(&file).with_context(|_| error::Io {
180            message: "canonicalizing root",
181            path: file.clone(),
182        })?;
183
184        Ok(Self {
185            original: file,
186            canonicalized,
187        })
188    }
189
190    fn check<P>(&self, path: P) -> Result<(), Error>
191    where
192        P: AsRef<Path>,
193    {
194        let path = path.as_ref();
195
196        let canonicalized = std::fs::canonicalize(path).with_context(|_| error::Io {
197            message: "canonicalizing include/import",
198            path: path.to_owned(),
199        })?;
200
201        // Don't allow directory traversals above the first file.
202        if canonicalized.starts_with(&self.canonicalized) {
203            Ok(())
204        } else {
205            error::DirectoryTraversal {
206                root: self.original.clone(),
207                file: path.to_owned(),
208            }
209            .fail()
210        }
211    }
212}
213
214#[must_use]
215struct PartialSource<'a, W> {
216    stack: &'a mut SourceStack<W>,
217    path: PathBuf,
218    scope: Scope,
219}
220
221impl<'a, W> PartialSource<'a, W> {
222    fn path(&self) -> &Path {
223        &self.path
224    }
225
226    fn push(self, nodes: Vec<Node>) -> &'a mut Source {
227        self.stack.sources.push(Source {
228            path: self.path,
229            nodes: nodes.into_iter(),
230            scope: self.scope,
231        });
232
233        self.stack.sources.last_mut().unwrap()
234    }
235}
236
237#[derive(Debug)]
238struct SourceStack<W> {
239    output: W,
240    sources: Vec<Source>,
241    root: Option<Root>,
242}
243
244impl<W> SourceStack<W> {
245    fn new(output: W) -> Self {
246        Self {
247            output,
248            sources: Default::default(),
249            root: Default::default(),
250        }
251    }
252
253    fn resolve(&mut self, path: PathBuf, scope: Scope) -> Result<PartialSource<W>, Error> {
254        ensure!(self.sources.len() <= 255, error::RecursionLimit);
255
256        let path = if let Some(ref root) = self.root {
257            let last = self.sources.last().unwrap();
258            let dir = match last.path.parent() {
259                Some(s) => s,
260                None => Path::new("./"),
261            };
262            let candidate = dir.join(path);
263            root.check(&candidate)?;
264            candidate
265        } else {
266            assert!(self.sources.is_empty());
267            self.root = Some(Root::new(path.clone())?);
268            path
269        };
270
271        Ok(PartialSource {
272            stack: self,
273            path,
274            scope,
275        })
276    }
277
278    fn peek(&mut self) -> Option<&mut Source> {
279        self.sources.last_mut()
280    }
281}
282
283impl<W> SourceStack<W>
284where
285    W: Write,
286{
287    fn pop(&mut self) -> Result<(), Error> {
288        let popped = self.sources.pop().unwrap();
289
290        if self.sources.is_empty() {
291            self.root = None;
292        }
293
294        let mut asm = match popped.scope {
295            Scope::Independent(a) => a,
296            Scope::Same => return Ok(()),
297        };
298
299        let raw = asm.take();
300        asm.finish()?;
301
302        if raw.is_empty() {
303            return Ok(());
304        }
305
306        if self.sources.is_empty() {
307            self.output.write_all(&raw).context(error::Io {
308                message: "writing output",
309                path: None,
310            })?;
311            Ok(())
312        } else {
313            self.write(RawOp::Raw(raw))
314        }
315    }
316
317    fn write(&mut self, mut op: RawOp) -> Result<(), Error> {
318        if self.sources.is_empty() {
319            panic!("no sources!");
320        }
321
322        for frame in self.sources[1..].iter_mut().rev() {
323            let asm = match frame.scope {
324                Scope::Same => continue,
325                Scope::Independent(ref mut a) => a,
326            };
327
328            if 0 == asm.push(op)? {
329                return Ok(());
330            } else {
331                op = RawOp::Raw(asm.take());
332            }
333        }
334
335        let first_asm = match self.sources[0].scope {
336            Scope::Independent(ref mut a) => a,
337            Scope::Same => panic!("sources[0] must be independent"),
338        };
339
340        first_asm.push(op)?;
341
342        Ok(())
343    }
344}
345
346/// A high-level interface for assembling files into EVM bytecode.
347///
348/// ## Example
349///
350/// ```rust
351/// use etk_asm::ingest::Ingest;
352/// #
353/// # use etk_asm::ingest::Error;
354/// #
355/// # use hex_literal::hex;
356///
357/// let text = r#"
358///     push2 lbl
359///     lbl:
360///     jumpdest
361/// "#;
362///
363/// let mut output = Vec::new();
364/// let mut ingest = Ingest::new(&mut output);
365/// ingest.ingest("./example.etk", &text)?;
366///
367/// # let expected = hex!("6100035b");
368/// # assert_eq!(output, expected);
369/// # Result::<(), Error>::Ok(())
370/// ```
371#[derive(Debug)]
372pub struct Ingest<W> {
373    sources: SourceStack<W>,
374}
375
376impl<W> Ingest<W> {
377    /// Make a new `Ingest` that writes assembled bytes to `output`.
378    pub fn new(output: W) -> Self {
379        Self {
380            sources: SourceStack::new(output),
381        }
382    }
383}
384
385impl<W> Ingest<W>
386where
387    W: Write,
388{
389    /// Assemble instructions from the file located at `path`.
390    pub fn ingest_file<P>(&mut self, path: P) -> Result<(), Error>
391    where
392        P: Into<PathBuf>,
393    {
394        let path = path.into();
395
396        let mut file = File::open(&path).with_context(|_| error::Io {
397            message: "opening source",
398            path: path.clone(),
399        })?;
400        let mut text = String::new();
401        file.read_to_string(&mut text).with_context(|_| error::Io {
402            message: "reading source",
403            path: path.clone(),
404        })?;
405
406        self.ingest(path, &text)
407    }
408
409    /// Assemble instructions from `src` as if they were read from a file located
410    /// at `path`.
411    pub fn ingest<P>(&mut self, path: P, src: &str) -> Result<(), Error>
412    where
413        P: Into<PathBuf>,
414    {
415        let nodes = parse_asm(src)?;
416        let partial = self.sources.resolve(path.into(), Scope::independent())?;
417        partial.push(nodes);
418
419        while let Some(source) = self.sources.peek() {
420            let node = match source.nodes.next() {
421                Some(n) => n,
422                None => {
423                    self.sources.pop()?;
424                    continue;
425                }
426            };
427
428            match node {
429                Node::Op(op) => {
430                    self.sources.write(RawOp::Op(op))?;
431                }
432                Node::Raw(raw) => {
433                    self.sources.write(RawOp::Raw(raw))?;
434                }
435                Node::Import(path) => {
436                    let partial = self.sources.resolve(path, Scope::same())?;
437                    let parsed = parse_file(partial.path())?;
438                    partial.push(parsed);
439                }
440                Node::Include(path) => {
441                    let partial = self.sources.resolve(path, Scope::independent())?;
442                    let parsed = parse_file(partial.path())?;
443                    partial.push(parsed);
444                }
445                Node::IncludeHex(path) => {
446                    let partial = self.sources.resolve(path, Scope::same())?;
447
448                    let file =
449                        std::fs::read_to_string(partial.path()).with_context(|_| error::Io {
450                            message: "reading hex include",
451                            path: partial.path().to_owned(),
452                        })?;
453
454                    let raw = hex::decode(file.trim())
455                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
456                        .context(error::InvalidHex {
457                            path: partial.path().to_owned(),
458                        })?;
459
460                    partial.push(vec![Node::Raw(raw)]);
461                }
462            }
463        }
464
465        if !self.sources.sources.is_empty() {
466            panic!("extra sources?");
467        }
468
469        Ok(())
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use assert_matches::assert_matches;
476
477    use crate::asm::Error as AsmError;
478
479    use hex_literal::hex;
480
481    use std::fmt::Display;
482    use std::io::Write;
483
484    use super::*;
485
486    use tempfile::NamedTempFile;
487
488    fn new_file<S: Display>(s: S) -> (NamedTempFile, PathBuf) {
489        let mut f = NamedTempFile::new().unwrap();
490        let root = f.path().parent().unwrap().join("root.asm");
491
492        write!(f, "{}", s).unwrap();
493        (f, root)
494    }
495
496    #[test]
497    fn ingest_import() -> Result<(), Error> {
498        let (f, root) = new_file("push1 42");
499
500        let text = format!(
501            r#"
502            push1 1
503            %import("{}")
504            push1 2
505        "#,
506            f.path().display()
507        );
508
509        let mut output = Vec::new();
510        let mut ingest = Ingest::new(&mut output);
511        ingest.ingest(root, &text)?;
512        assert_eq!(output, hex!("6001602a6002"));
513
514        Ok(())
515    }
516
517    #[test]
518    fn ingest_include() -> Result<(), Error> {
519        let (f, root) = new_file(
520            r#"
521                a:
522                jumpdest
523                pc
524                push1 a
525                jump
526            "#,
527        );
528
529        let text = format!(
530            r#"
531            push1 1
532            %include("{}")
533            push1 2
534        "#,
535            f.path().display()
536        );
537
538        let mut output = Vec::new();
539        let mut ingest = Ingest::new(&mut output);
540        ingest.ingest(root, &text)?;
541        assert_eq!(output, hex!("60015b586000566002"));
542
543        Ok(())
544    }
545
546    #[test]
547    fn ingest_import_twice() {
548        let (f, root) = new_file(
549            r#"
550                a:
551                jumpdest
552                push1 a
553            "#,
554        );
555
556        let text = format!(
557            r#"
558                push1 1
559                %import("{0}")
560                %import("{0}")
561                push1 2
562            "#,
563            f.path().display()
564        );
565
566        let mut output = Vec::new();
567        let mut ingest = Ingest::new(&mut output);
568        let err = ingest.ingest(root, &text).unwrap_err();
569
570        assert_matches!(
571            err,
572            Error::Assemble {
573                source: AsmError::DuplicateLabel { label, ..}
574            } if label == "a"
575        );
576    }
577
578    #[test]
579    fn ingest_include_hex() -> Result<(), Error> {
580        let (f, root) = new_file("deadbeef0102f6");
581
582        let text = format!(
583            r#"
584                push1 1
585                %include_hex("{}")
586                push1 2
587            "#,
588            f.path().display(),
589        );
590
591        let mut output = Vec::new();
592        let mut ingest = Ingest::new(&mut output);
593        ingest.ingest(root, &text)?;
594        assert_eq!(output, hex!("6001deadbeef0102f66002"));
595
596        Ok(())
597    }
598
599    #[test]
600    fn ingest_include_hex_label() -> Result<(), Error> {
601        let (f, root) = new_file("deadbeef0102f6");
602
603        let text = format!(
604            r#"
605                push1 1
606                %include_hex("{}")
607                a:
608                jumpdest
609                push1 a
610                push1 0xff
611            "#,
612            f.path().display(),
613        );
614
615        let mut output = Vec::new();
616        let mut ingest = Ingest::new(&mut output);
617        ingest.ingest(root, &text)?;
618        assert_eq!(output, hex!("6001deadbeef0102f65b600960ff"));
619
620        Ok(())
621    }
622
623    #[test]
624    fn ingest_pending_then_raw() -> Result<(), Error> {
625        let (f, root) = new_file("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
626
627        let text = format!(
628            r#"
629                push2 lbl
630                %include_hex("{}")
631                lbl:
632                jumpdest
633            "#,
634            f.path().display(),
635        );
636
637        let mut output = Vec::new();
638        let mut ingest = Ingest::new(&mut output);
639        ingest.ingest(root, &text)?;
640
641        let expected = hex!("61001caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa5b");
642        assert_eq!(output, expected);
643
644        Ok(())
645    }
646
647    #[test]
648    fn ingest_import_in_import() -> Result<(), Error> {
649        let (end, _) = new_file(
650            r#"
651                end:
652                jumpdest
653                push1 start
654                push1 middle
655            "#,
656        );
657
658        let (middle, root) = new_file(format!(
659            r#"
660                %import("{}")
661                middle:
662                jumpdest
663                push2 start
664                push2 end
665            "#,
666            end.path().display(),
667        ));
668
669        let text = format!(
670            r#"
671                push3 end
672                push3 middle
673                start:
674                jumpdest
675                %import("{}")
676            "#,
677            middle.path().display(),
678        );
679
680        let mut output = Vec::new();
681        let mut ingest = Ingest::new(&mut output);
682        ingest.ingest(root, &text)?;
683
684        let expected = hex!("620000096200000e5b5b6008600e5b610008610009");
685        assert_eq!(output, expected);
686
687        Ok(())
688    }
689
690    #[test]
691    fn ingest_import_in_include() -> Result<(), Error> {
692        let (end, _) = new_file(
693            r#"
694                included:
695                jumpdest
696                push2 backward
697                push2 forward
698            "#,
699        );
700
701        let (middle, root) = new_file(format!(
702            r#"
703                pc
704                push1 backward
705                forward:
706                jumpdest
707                %import("{}")
708                backward:
709                jumpdest
710                push1 forward
711                push1 included
712            "#,
713            end.path().display(),
714        ));
715
716        let text = format!(
717            r#"
718                push3 backward
719                forward:
720                jumpdest
721                %include("{}")
722                backward:
723                jumpdest
724                push3 forward
725            "#,
726            middle.path().display(),
727        );
728
729        let mut output = Vec::new();
730        let mut ingest = Ingest::new(&mut output);
731        ingest.ingest(root, &text)?;
732
733        let expected = hex!("620000155b58600b5b5b61000b6100035b600360045b62000004");
734        assert_eq!(output, expected);
735
736        Ok(())
737    }
738
739    #[test]
740    fn ingest_directory_traversal() {
741        let (f, _) = new_file("pc");
742
743        let text = format!(
744            r#"
745                %include("{}")
746            "#,
747            f.path().display(),
748        );
749
750        let mut output = Vec::new();
751        let mut ingest = Ingest::new(&mut output);
752        let root = std::env::current_exe().unwrap();
753        let err = ingest.ingest(root, &text).unwrap_err();
754
755        assert_matches!(err, Error::DirectoryTraversal { .. });
756    }
757
758    #[test]
759    fn ingest_recursive() {
760        let (mut f, root) = new_file("");
761        let path = f.path().display().to_string();
762        write!(f, r#"%import("{}")"#, path).unwrap();
763
764        let text = format!(
765            r#"
766                %import("{}")
767            "#,
768            path,
769        );
770
771        let mut output = Vec::new();
772        let mut ingest = Ingest::new(&mut output);
773        let err = ingest.ingest(root, &text).unwrap_err();
774
775        assert_matches!(err, Error::RecursionLimit { .. });
776    }
777}