Rewrite the project

The previous project architecture was far too complicated and hard to
maintain. The new one is much more simple. Although procedural macros
are cools, they are a no-go on Rust-OpenSMTPD.

Reports and filter are implemented (except data-line) but untested.
This commit is contained in:
Rodolphe Bréard 2020-11-25 18:04:16 +01:00
parent fc072743ad
commit a6d4dd21c1
48 changed files with 1723 additions and 1493 deletions

View file

@ -1,298 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::errors::Error;
use nom::branch::alt;
use nom::bytes::streaming::{tag, take_till};
use nom::character::streaming::{digit1, hex_digit1, line_ending};
use nom::combinator::{map_res, value};
use nom::multi::many0;
use nom::Err::Incomplete;
use nom::IResult;
use std::str::FromStr;
pub type SessionId = u64;
pub type Token = u64;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Version {
V1,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Kind {
Report,
Filter,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Subsystem {
SmtpIn,
SmtpOut,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Event {
LinkAuth,
LinkConnect,
LinkDisconnect,
LinkIdentify,
LinkReset,
LinkTls,
TxBegin,
TxMail,
TxRcpt,
TxEnvelope,
TxData,
TxCommit,
TxRollback,
ProtocolClient,
ProtocolServer,
FilterResponse,
Timeout,
}
impl FromStr for Event {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
let s = if !s.contains('-') {
s.replace("link", "link-")
.replace("tx", "tx-")
.replace("protocol", "protocol-")
.replace("filter", "filter-")
} else {
s
};
let (_, evt) = parse_event(&s)?;
Ok(evt)
}
}
impl ToString for Event {
fn to_string(&self) -> String {
let s = match self {
Event::LinkAuth => "link-auth",
Event::LinkConnect => "link-connect",
Event::LinkDisconnect => "link-disconnect",
Event::LinkIdentify => "link-identify",
Event::LinkReset => "link-reset",
Event::LinkTls => "link-tls",
Event::TxBegin => "tx-begin",
Event::TxMail => "tx-mail",
Event::TxRcpt => "tx-rcpt",
Event::TxEnvelope => "tx-envelope",
Event::TxData => "tx-data",
Event::TxCommit => "tx-commit",
Event::TxRollback => "tx-rollback",
Event::ProtocolClient => "protocol-client",
Event::ProtocolServer => "protocol-server",
Event::FilterResponse => "filter-response",
Event::Timeout => "timeout",
};
String::from(s)
}
}
#[derive(Debug)]
pub struct TimeVal {
pub sec: i64,
pub usec: i64,
}
impl ToString for TimeVal {
fn to_string(&self) -> String {
format!("{}.{}", self.sec, self.usec)
}
}
#[derive(Debug)]
pub enum Entry {
V1Report(V1Report),
V1Filter(V1Filter),
}
impl Entry {
pub fn new(entry: &str) -> Result<(String, Option<Self>), Error> {
match parse_entry(entry) {
Ok((remainder, entry)) => Ok((remainder.to_string(), Some(entry))),
Err(e) => match e {
Incomplete(_) => Ok((String::new(), None)),
_ => Err(e.into()),
},
}
}
pub fn get_event(&self) -> Event {
match self {
Entry::V1Report(r) => r.event.to_owned(),
Entry::V1Filter(f) => f.event.to_owned(),
}
}
pub fn get_session_id(&self) -> SessionId {
match self {
Entry::V1Report(r) => r.session_id,
Entry::V1Filter(f) => f.session_id,
}
}
pub fn is_disconnect(&self) -> bool {
match self {
Entry::V1Report(r) => r.event == Event::LinkDisconnect,
Entry::V1Filter(_) => false,
}
}
}
#[derive(Debug)]
pub struct V1Report {
pub timestamp: TimeVal,
pub subsystem: Subsystem,
pub event: Event,
pub session_id: SessionId,
pub params: Vec<String>,
}
#[derive(Debug)]
pub struct V1Filter {
pub timestamp: TimeVal,
pub subsystem: Subsystem,
pub event: Event,
pub session_id: SessionId,
pub token: Token,
pub params: Vec<String>,
}
fn separator(input: &str) -> IResult<&str, &str> {
tag("|")(input)
}
fn parse_kind(input: &str) -> IResult<&str, Kind> {
alt((
value(Kind::Report, tag("report")),
value(Kind::Filter, tag("filter")),
))(input)
}
fn parse_version(input: &str) -> IResult<&str, Version> {
value(Version::V1, tag("1"))(input)
}
fn parse_timestamp(input: &str) -> IResult<&str, TimeVal> {
let (input, sec) = map_res(digit1, |s: &str| s.parse::<i64>())(input)?;
let (input, _) = tag(".")(input)?;
let (input, usec) = map_res(digit1, |s: &str| s.parse::<i64>())(input)?;
let timestamp = TimeVal { sec, usec };
Ok((input, timestamp))
}
fn parse_subsystem(input: &str) -> IResult<&str, Subsystem> {
alt((
value(Subsystem::SmtpIn, tag("smtp-in")),
value(Subsystem::SmtpOut, tag("smtp-out")),
))(input)
}
fn parse_event(input: &str) -> IResult<&str, Event> {
alt((
value(Event::LinkAuth, tag("link-auth")),
value(Event::LinkConnect, tag("link-connect")),
value(Event::LinkDisconnect, tag("link-disconnect")),
value(Event::LinkIdentify, tag("link-identify")),
value(Event::LinkReset, tag("link-reset")),
value(Event::LinkTls, tag("link-tls")),
value(Event::TxBegin, tag("tx-begin")),
value(Event::TxMail, tag("tx-mail")),
value(Event::TxRcpt, tag("tx-rcpt")),
value(Event::TxEnvelope, tag("tx-envelope")),
value(Event::TxData, tag("tx-data")),
value(Event::TxCommit, tag("tx-commit")),
value(Event::TxRollback, tag("tx-rollback")),
value(Event::ProtocolClient, tag("protocol-client")),
value(Event::ProtocolServer, tag("protocol-server")),
value(Event::FilterResponse, tag("filter-response")),
value(Event::Timeout, tag("timeout")),
))(input)
}
fn parse_token(input: &str) -> IResult<&str, Token> {
map_res(hex_digit1, |s: &str| Token::from_str_radix(s, 16))(input)
}
fn parse_session_id(input: &str) -> IResult<&str, SessionId> {
map_res(hex_digit1, |s: &str| SessionId::from_str_radix(s, 16))(input)
}
fn parse_param(input: &str) -> IResult<&str, String> {
let (input, _) = separator(input)?;
let (input, param) = take_till(is_end_param)(input)?;
Ok((input, param.to_string()))
}
fn is_end_param(c: char) -> bool {
c == '|' || c == '\r' || c == '\n'
}
fn parse_v1_report(input: &str) -> IResult<&str, Entry> {
let (input, timestamp) = parse_timestamp(input)?;
let (input, _) = separator(input)?;
let (input, subsystem) = parse_subsystem(input)?;
let (input, _) = separator(input)?;
let (input, event) = parse_event(input)?;
let (input, _) = separator(input)?;
let (input, session_id) = parse_session_id(input)?;
let (input, params) = many0(parse_param)(input)?;
let (input, _) = line_ending(input)?;
let report = V1Report {
timestamp,
subsystem,
event,
session_id,
params,
};
Ok((input, Entry::V1Report(report)))
}
fn parse_v1_filter(input: &str) -> IResult<&str, Entry> {
let (input, timestamp) = parse_timestamp(input)?;
let (input, _) = separator(input)?;
let (input, subsystem) = parse_subsystem(input)?;
let (input, _) = separator(input)?;
let (input, event) = parse_event(input)?;
let (input, _) = separator(input)?;
let (input, session_id) = parse_session_id(input)?;
let (input, _) = separator(input)?;
let (input, token) = parse_token(input)?;
let (input, params) = many0(parse_param)(input)?;
let (input, _) = line_ending(input)?;
let filter = V1Filter {
timestamp,
subsystem,
event,
session_id,
token,
params,
};
Ok((input, Entry::V1Filter(filter)))
}
fn parse_entry(input: &str) -> IResult<&str, Entry> {
let (input, kind) = parse_kind(input)?;
let (input, _) = separator(input)?;
let (input, version) = parse_version(input)?;
let (input, _) = separator(input)?;
let (input, entry) = match version {
Version::V1 => match kind {
Kind::Report => parse_v1_report(input)?,
Kind::Filter => parse_v1_filter(input)?,
},
};
Ok((input, entry))
}

View file

@ -1,62 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::fmt;
pub struct Error {
message: String,
}
impl Error {
pub fn new(msg: &str) -> Self {
Error {
message: msg.to_string(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error::new(&format!("IO error: {}", error))
}
}
impl From<std::string::String> for Error {
fn from(error: std::string::String) -> Self {
Error { message: error }
}
}
impl From<std::str::Utf8Error> for Error {
fn from(error: std::str::Utf8Error) -> Self {
Error::new(&format!("UTF8 error: {}", error))
}
}
impl From<log::SetLoggerError> for Error {
fn from(error: log::SetLoggerError) -> Self {
Error::new(&format!("Logger error: {}", error))
}
}
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
fn from(error: nom::Err<(&str, nom::error::ErrorKind)>) -> Self {
let msg = match error {
nom::Err::Incomplete(_) => "not enough data".to_string(),
nom::Err::Error(c) => format!("{:?}", c),
nom::Err::Failure(c) => format!("{:?}", c),
};
Error::new(&format!("Parsing error: {}", msg))
}
}

View file

@ -1,85 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::entry::{Entry, Event, Kind, Subsystem, Version};
use crate::errors::Error;
use crate::output::FilterOutput;
use std::collections::HashSet;
macro_rules! handle {
($self: ident, $obj: ident, $version: expr, $kind: expr, $entry: ident, $output: ident, $session_ctx: ident, $filter_ctx: ident) => {{
if $self.version == $version
&& $self.kind == $kind
&& $self.subsystem == $obj.subsystem
&& $self.events.contains(&$obj.event)
{
($self.action)($output, $entry, $session_ctx, $filter_ctx)?;
}
Ok(())
}};
}
type Callback<S, F> = fn(&mut dyn FilterOutput, &Entry, &mut S, &mut F) -> Result<(), String>;
#[derive(Clone)]
pub struct Handler<S, F> {
version: Version,
pub(crate) kind: Kind,
pub(crate) subsystem: Subsystem,
pub(crate) events: HashSet<Event>,
action: Callback<S, F>,
}
impl<S, F> Handler<S, F> {
pub fn new(
version: Version,
kind: Kind,
subsystem: Subsystem,
events: &[Event],
action: Callback<S, F>,
) -> Self {
Handler {
version,
kind,
subsystem,
events: events.iter().cloned().collect(),
action,
}
}
pub fn send(
&self,
entry: &Entry,
output: &mut dyn FilterOutput,
session_ctx: &mut S,
filter_ctx: &mut F,
) -> Result<(), Error> {
match entry {
Entry::V1Report(report) => handle!(
self,
report,
Version::V1,
Kind::Report,
entry,
output,
session_ctx,
filter_ctx
),
Entry::V1Filter(filter) => handle!(
self,
filter,
Version::V1,
Kind::Filter,
entry,
output,
session_ctx,
filter_ctx
),
}
}
}

View file

@ -1,17 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::entry::Entry;
use crate::errors::Error;
pub trait FilterInput {
fn next(&mut self) -> Result<Entry, Error>;
}
mod stdin;
pub use stdin::StdIn;

View file

@ -1,78 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::entry::Entry;
use crate::errors::Error;
use crate::input::FilterInput;
use std::default::Default;
use std::io::{self, ErrorKind, Read};
use std::str;
const BUFFER_SIZE: usize = 4096;
pub struct StdIn {
buffer: [u8; BUFFER_SIZE],
stdin: io::Stdin,
input: String,
}
impl Default for StdIn {
fn default() -> Self {
StdIn {
buffer: [0; BUFFER_SIZE],
stdin: io::stdin(),
input: String::new(),
}
}
}
impl FilterInput for StdIn {
fn next(&mut self) -> Result<Entry, Error> {
let mut force_read = false;
loop {
if force_read || self.input.is_empty() {
// Reset the flag
force_read = false;
// Read stdin in self.buffer
self.buffer.copy_from_slice(&[0; BUFFER_SIZE]);
let len = match self.stdin.read(&mut self.buffer) {
Ok(n) => n,
Err(e) => match e.kind() {
ErrorKind::Interrupted => {
continue;
}
_ => {
return Err(e.into());
}
},
};
if len == 0 {
return Err(Error::new("Unable to read on stdin."));
}
// Put the buffer's content in self.input
self.input += match self.buffer.iter().position(|&x| x == 0) {
Some(i) => str::from_utf8(&self.buffer[..i]),
None => str::from_utf8(&self.buffer),
}?;
}
// Try to build an entry from self.input
let (remainder, entry_opt) = Entry::new(&self.input)?;
match entry_opt {
// We have at least one entry.
Some(entry) => {
self.input = remainder;
return Ok(entry);
}
// The data is incomplete, no entry could be built.
None => {
force_read = true;
}
};
}
}
}

View file

@ -1,234 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
mod errors;
mod handler;
mod logger;
pub mod entry;
pub mod input;
pub mod output;
use crate::entry::{Kind, SessionId, Subsystem};
use log;
use std::collections::{HashMap, HashSet};
use std::default::Default;
pub use crate::errors::Error;
pub use crate::handler::Handler;
pub use crate::logger::SmtpdLogger;
pub use opensmtpd_derive::report;
#[macro_export]
macro_rules! register_contexts {
($context: ty) => {
opensmtpd::register_contexts!($context, $context);
};
($session_context: ty, $filter_context: ty) => {
type OpenSmtpdFilterContextType = $filter_context;
type OpenSmtpdSessionContextType = $session_context;
};
}
#[macro_export]
macro_rules! register_filter_context_only {
($context: ty) => {
type OpenSmtpdFilterContextType = $context;
type OpenSmtpdSessionContextType = opensmtpd::NoContext;
};
}
#[macro_export]
macro_rules! register_session_context_only {
($context: ty) => {
type OpenSmtpdFilterContextType = opensmtpd::NoContext;
type OpenSmtpdSessionContextType = $context;
};
}
#[macro_export]
macro_rules! register_no_context {
() => {
type OpenSmtpdFilterContextType = opensmtpd::NoContext;
type OpenSmtpdSessionContextType = opensmtpd::NoContext;
};
}
#[macro_export]
macro_rules! simple_filter {
($handlers: expr) => {
opensmtpd::simple_filter!(
log::Level::Info,
opensmtpd::NoContext,
opensmtpd::NoContext,
$handlers
);
};
($filter_ctx: ty, $handlers: expr) => {
opensmtpd::simple_filter!(
log::Level::Info,
opensmtpd::NoContext,
$filter_ctx,
$handlers
);
};
($sesion_ctx: ty, $filter_ctx: ty, $handlers: expr) => {
opensmtpd::simple_filter!(log::Level::Info, $sesion_ctx, $filter_ctx, $handlers);
};
($log_level: path, $sesion_ctx: ty, $filter_ctx: ty, $handlers: expr) => {
let handlers = ($handlers)
.iter()
.map(|f| f())
.collect::<Vec<opensmtpd::Handler<$sesion_ctx, $filter_ctx>>>();
let _ = opensmtpd::SmtpdLogger::new().set_level($log_level).init();
opensmtpd::Filter::<
opensmtpd::input::StdIn,
opensmtpd::output::StdOut,
$sesion_ctx,
$filter_ctx,
>::default()
.set_handlers(handlers.as_slice())
.register_events()
.run();
};
}
macro_rules! fatal_error {
($error: ident) => {
log::error!("Error: {}", $error);
std::process::exit(1);
};
}
macro_rules! insert_events {
($handler: ident, $set: ident) => {{
for e in $handler.events.iter() {
$set.insert(e);
}
}};
}
macro_rules! register_events {
($output: expr, $set: ident, $kind: expr, $subsystem: expr) => {
for e in $set.iter() {
let msg = format!("register|{}|{}|{}", $kind, $subsystem, e.to_string());
if let Err(e) = $output.send(&msg) {
fatal_error!(e);
};
}
};
}
#[derive(Clone, Default)]
pub struct NoContext;
pub struct Filter<I, O, S, F>
where
I: crate::input::FilterInput + Default,
O: crate::output::FilterOutput + Default,
S: Default,
F: Default,
{
input: I,
output: O,
session_ctx: HashMap<SessionId, S>,
filter_ctx: F,
handlers: Vec<Handler<S, F>>,
}
impl<I, O, S, F> Default for Filter<I, O, S, F>
where
I: crate::input::FilterInput + Default,
O: crate::output::FilterOutput + Default,
S: Default,
F: Default,
{
fn default() -> Self {
Filter {
input: I::default(),
output: O::default(),
session_ctx: HashMap::new(),
filter_ctx: F::default(),
handlers: Vec::new(),
}
}
}
impl<I, O, S, F> Filter<I, O, S, F>
where
I: crate::input::FilterInput + Default,
O: crate::output::FilterOutput + Default,
S: Clone + Default,
F: Clone + Default,
{
pub fn set_handlers(&mut self, handlers: &[Handler<S, F>]) -> &mut Self {
self.handlers = handlers.to_vec();
self
}
pub fn register_events(&mut self) -> &mut Self {
let mut report_smtp_in = HashSet::new();
let mut report_smtp_out = HashSet::new();
let mut filter_smtp_in = HashSet::new();
let mut filter_smtp_out = HashSet::new();
for h in self.handlers.iter() {
match h.kind {
Kind::Report => match h.subsystem {
Subsystem::SmtpIn => insert_events!(h, report_smtp_in),
Subsystem::SmtpOut => insert_events!(h, report_smtp_out),
},
Kind::Filter => match h.subsystem {
Subsystem::SmtpIn => insert_events!(h, filter_smtp_in),
Subsystem::SmtpOut => insert_events!(h, filter_smtp_out),
},
};
}
register_events!(self.output, report_smtp_in, "report", "smtp-in");
register_events!(self.output, report_smtp_out, "report", "smtp-out");
register_events!(self.output, filter_smtp_in, "filter", "smtp-in");
register_events!(self.output, filter_smtp_out, "filter", "smtp-out");
self
}
pub fn run(&mut self) {
loop {
match self.input.next() {
Ok(entry) => {
log::debug!("{:?}", entry);
let session_id = entry.get_session_id();
let mut session_ctx = match self.session_ctx.get_mut(&session_id) {
Some(c) => c,
None => {
self.session_ctx.insert(session_id, S::default());
self.session_ctx.get_mut(&session_id).unwrap()
}
};
for h in self.handlers.iter() {
match h.send(
&entry,
&mut self.output,
&mut session_ctx,
&mut self.filter_ctx,
) {
Ok(_) => {}
Err(e) => {
log::warn!("Warning: {}", e);
}
};
}
if entry.is_disconnect() {
self.session_ctx.remove(&session_id);
}
}
Err(e) => {
fatal_error!(e);
}
};
}
}
}

View file

@ -1,56 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::errors::Error;
use log::{Level, Metadata, Record};
use std::io::{self, Write};
#[derive(Clone)]
pub struct SmtpdLogger {
level: Level,
}
impl SmtpdLogger {
pub fn new() -> Self {
SmtpdLogger::default()
}
pub fn set_level(&mut self, level: Level) -> Self {
self.level = level;
self.clone()
}
pub fn init(self) -> Result<(), Error> {
let level = self.level.to_level_filter();
log::set_boxed_logger(Box::new(self))?;
log::set_max_level(level);
Ok(())
}
}
impl Default for SmtpdLogger {
fn default() -> Self {
SmtpdLogger { level: Level::Warn }
}
}
impl log::Log for SmtpdLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.level
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let stderr = io::stderr();
let mut handle = stderr.lock();
writeln!(handle, "{}: {}", record.level(), record.args()).unwrap();
}
}
fn flush(&self) {}
}

View file

@ -1,19 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::errors::Error;
pub trait FilterOutput {
fn send(&mut self, msg: &str) -> Result<(), Error>;
}
mod null;
mod stdout;
pub use null::NullOutput;
pub use stdout::{StdErr, StdOut};

View file

@ -1,25 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::errors::Error;
use crate::output::FilterOutput;
use std::default::Default;
pub struct NullOutput {}
impl Default for NullOutput {
fn default() -> Self {
NullOutput {}
}
}
impl FilterOutput for NullOutput {
fn send(&mut self, _msg: &str) -> Result<(), Error> {
Ok(())
}
}

View file

@ -1,33 +0,0 @@
// Copyright (c) 2019 Rodolphe Bréard
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::errors::Error;
use crate::output::FilterOutput;
use std::default::Default;
macro_rules! new_stdout {
($name: ident, $out: ident) => {
pub struct $name {}
impl Default for $name {
fn default() -> Self {
$name {}
}
}
impl FilterOutput for $name {
fn send(&mut self, msg: &str) -> Result<(), Error> {
$out!("{}", msg);
Ok(())
}
}
};
}
new_stdout!(StdOut, println);
new_stdout!(StdErr, eprintln);