<commit_before>//! Extensions to the parsing API with niche applicability.

use super::*;

/// Extensions to the `ParseStream` API to support speculative parsing.
pub trait Speculative {
    /// Advance this parse stream to the position of a forked parse stream.
    ///
    /// This is the opposite operation to [`ParseStream::fork`]. You can fork a
    /// parse stream, perform some speculative parsing, then join the original
    /// stream to the fork to "commit" the parsing from the fork to the main
    /// stream.
    ///
    /// If you can avoid doing this, you should, as it limits the ability to
    /// generate useful errors. That said, it is often the only way to parse
    /// syntax of the form `A* B*` for arbitrary syntax `A` and `B`. The problem
    /// is that when the fork fails to parse an `A`, it's impossible to tell
    /// whether that was because of a syntax error and the user meant to provide
    /// an `A`, or that the `A`s are finished and its time to start parsing
    /// `B`s. Use with care.
    ///
    /// Also note that if `A` is a subset of `B`, `A* B*` can be parsed by
    /// parsing `B*` and removing the leading members of `A` from the
    /// repetition, bypassing the need to involve the downsides associated with
    /// speculative parsing.
    ///
    /// [`ParseStream::fork`]: ParseBuffer::fork
    ///
    /// # Example
    ///
    /// There has been chatter about the possibility of making the colons in the
    /// turbofish syntax like `path::to::<T>` no longer required by accepting
    /// `path::to<T>` in expression position. Specifically, according to [RFC
    /// 2544], [`PathSegment`] parsing should always try to consume a following
    /// `<` token as the start of generic arguments, and reset to the `<` if
    /// that fails (e.g. the token is acting as a less-than operator).
    ///
    /// This is the exact kind of parsing behavior which requires the "fork,
    /// try, commit" behavior that [`ParseStream::fork`] discourages. With
    /// `advance_to`, we can avoid having to parse the speculatively parsed
    /// content a second time.
    ///
    /// This change in behavior can be implemented in syn by replacing just the
    /// `Parse` implementation for `PathSegment`:
    ///
    /// ```
    /// # use syn::ext::IdentExt;
    /// use syn::parse::discouraged::Speculative;
    /// # use syn::parse::{Parse, ParseStream};
    /// # use syn::{Ident, PathArguments, Result, Token};
    ///
    /// pub struct PathSegment {
    ///     pub ident: Ident,
    ///     pub arguments: PathArguments,
    /// }
    /// #
    /// # impl<T> From<T> for PathSegment
    /// # where
    /// #     T: Into<Ident>,
    /// # {
    /// #     fn from(ident: T) -> Self {
    /// #         PathSegment {
    /// #             ident: ident.into(),
    /// #             arguments: PathArguments::None,
    /// #         }
    /// #     }
    /// # }
    ///
    /// impl Parse for PathSegment {
    ///     fn parse(input: ParseStream) -> Result<Self> {
    ///         if input.peek(Token![super])
    ///             || input.peek(Token![self])
    ///             || input.peek(Token![Self])
    ///             || input.peek(Token![crate])
    ///             || input.peek(Token![extern])
    ///         {
    ///             let ident = input.call(Ident::parse_any)?;
    ///             return Ok(PathSegment::from(ident));
    ///         }
    ///
    ///         let ident = input.parse()?;
    ///         if input.peek(Token![::]) && input.peek3(Token![<]) {
    ///             return Ok(PathSegment {
    ///                 ident,
    ///                 arguments: PathArguments::AngleBracketed(input.parse()?),
    ///             });
    ///         }
    ///         if input.peek(Token![<]) && !input.peek(Token![<=]) {
    ///             let fork = input.fork();
    ///             if let Ok(arguments) = fork.parse() {
    ///                 input.advance_to(&fork);
    ///                 return Ok(PathSegment {
    ///                     ident,
    ///                     arguments: PathArguments::AngleBracketed(arguments),
    ///                 });
    ///             }
    ///         }
    ///         Ok(PathSegment::from(ident))
    ///     }
    /// }
    ///
    /// # syn::parse_str::<PathSegment>("a<b,c>").unwrap();
    /// ```
    ///
    /// # Drawbacks
    ///
    /// The main drawback of this style of speculative parsing is in error
    /// presentation. Even if the lookahead is the "correct" parse, the error
    /// that is shown is that of the "fallback" parse. To use the same example
    /// as the turbofish above, take the following unfinished "turbofish":
    ///
    /// ```text
    /// let _ = f<&'a fn(), for<'a> serde::>();
    /// ```
    ///
    /// If this is parsed as generic arguments, we can provide the error message
    ///
    /// ```text
    /// error: expected identifier
    ///  --> src.rs:L:C
    ///   |
    /// L | let _ = f<&'a fn(), for<'a> serde::>();
    ///   |                                    ^
    /// ```
    ///
    /// but if parsed using the above speculative parsing, it falls back to
    /// assuming that the `<` is a less-than when it fails to parse the generic
    /// arguments, and tries to interpret the `&'a` as the start of a labelled
    /// loop, resulting in the much less helpful error
    ///
    /// ```text
    /// error: expected `:`
    ///  --> src.rs:L:C
    ///   |
    /// L | let _ = f<&'a fn(), for<'a> serde::>();
    ///   |               ^^
    /// ```
    ///
    /// This can be mitigated with various heuristics (two examples: show both
    /// forks' parse errors, or show the one that consumed more tokens), but
    /// when you can control the grammar, sticking to something that can be
    /// parsed LL(3) and without the LL(*) speculative parsing this makes
    /// possible, displaying reasonable errors becomes much more simple.
    ///
    /// [RFC 2544]: https://github.com/rust-lang/rfcs/pull/2544
    /// [`PathSegment`]: crate::PathSegment
    ///
    /// # Performance
    ///
    /// This method performs a cheap fixed amount of work that does not depend
    /// on how far apart the two streams are positioned.
    ///
    /// # Panics
    ///
    /// The forked stream in the argument of `advance_to` must have been
    /// obtained by forking `self`. Attempting to advance to any other stream
    /// will cause a panic.
    fn advance_to(&self, fork: &Self);
}

impl<'a> Speculative for ParseBuffer<'a> {
    fn advance_to(&self, fork: &Self) {
        if !crate::buffer::same_scope(self.cursor(), fork.cursor()) {
            panic!("Fork was not derived from the advancing parse stream");
        }

        let (self_unexp, self_sp) = inner_unexpected(self);
        let (fork_unexp, fork_sp) = inner_unexpected(fork);
        if !Rc::ptr_eq(&self_unexp, &fork_unexp) {
            match (fork_sp, self_sp) {
                // Unexpected set on the fork, but not on `self`, copy it over.
                (Some(span), None) => {
                    self_unexp.set(Unexpected::Some(span));
                }
                // Unexpected unset. Use chain to propagate errors from fork.
                (None, None) => {
                    fork_unexp.set(Unexpected::Chain(self_unexp));

                    // Ensure toplevel 'unexpected' tokens from the fork don't
                    // bubble up the chain by replacing the root `unexpected`
                    // pointer, only 'unexpected' tokens from existing group
                    // parsers should bubble.
                    fork.unexpected
                        .set(Some(Rc::new(Cell::new(Unexpected::None))));
                }
                // Unexpected has been set on `self`. No changes needed.
                (_, Some(_)) => {}
            }
        }

        // See comment on `cell` in the struct definition.
        self.cell
            .set(unsafe { mem::transmute::<Cursor, Cursor<'static>>(fork.cursor()) })
    }
}
<commit_msg>Fix typo in Speculative::advance_to documentation<commit_after>//! Extensions to the parsing API with niche applicability.

use super::*;

/// Extensions to the `ParseStream` API to support speculative parsing.
pub trait Speculative {
    /// Advance this parse stream to the position of a forked parse stream.
    ///
    /// This is the opposite operation to [`ParseStream::fork`]. You can fork a
    /// parse stream, perform some speculative parsing, then join the original
    /// stream to the fork to "commit" the parsing from the fork to the main
    /// stream.
    ///
    /// If you can avoid doing this, you should, as it limits the ability to
    /// generate useful errors. That said, it is often the only way to parse
    /// syntax of the form `A* B*` for arbitrary syntax `A` and `B`. The problem
    /// is that when the fork fails to parse an `A`, it's impossible to tell
    /// whether that was because of a syntax error and the user meant to provide
    /// an `A`, or that the `A`s are finished and it's time to start parsing
    /// `B`s. Use with care.
    ///
    /// Also note that if `A` is a subset of `B`, `A* B*` can be parsed by
    /// parsing `B*` and removing the leading members of `A` from the
    /// repetition, bypassing the need to involve the downsides associated with
    /// speculative parsing.
    ///
    /// [`ParseStream::fork`]: ParseBuffer::fork
    ///
    /// # Example
    ///
    /// There has been chatter about the possibility of making the colons in the
    /// turbofish syntax like `path::to::<T>` no longer required by accepting
    /// `path::to<T>` in expression position. Specifically, according to [RFC
    /// 2544], [`PathSegment`] parsing should always try to consume a following
    /// `<` token as the start of generic arguments, and reset to the `<` if
    /// that fails (e.g. the token is acting as a less-than operator).
    ///
    /// This is the exact kind of parsing behavior which requires the "fork,
    /// try, commit" behavior that [`ParseStream::fork`] discourages. With
    /// `advance_to`, we can avoid having to parse the speculatively parsed
    /// content a second time.
    ///
    /// This change in behavior can be implemented in syn by replacing just the
    /// `Parse` implementation for `PathSegment`:
    ///
    /// ```
    /// # use syn::ext::IdentExt;
    /// use syn::parse::discouraged::Speculative;
    /// # use syn::parse::{Parse, ParseStream};
    /// # use syn::{Ident, PathArguments, Result, Token};
    ///
    /// pub struct PathSegment {
    ///     pub ident: Ident,
    ///     pub arguments: PathArguments,
    /// }
    /// #
    /// # impl<T> From<T> for PathSegment
    /// # where
    /// #     T: Into<Ident>,
    /// # {
    /// #     fn from(ident: T) -> Self {
    /// #         PathSegment {
    /// #             ident: ident.into(),
    /// #             arguments: PathArguments::None,
    /// #         }
    /// #     }
    /// # }
    ///
    /// impl Parse for PathSegment {
    ///     fn parse(input: ParseStream) -> Result<Self> {
    ///         if input.peek(Token![super])
    ///             || input.peek(Token![self])
    ///             || input.peek(Token![Self])
    ///             || input.peek(Token![crate])
    ///             || input.peek(Token![extern])
    ///         {
    ///             let ident = input.call(Ident::parse_any)?;
    ///             return Ok(PathSegment::from(ident));
    ///         }
    ///
    ///         let ident = input.parse()?;
    ///         if input.peek(Token![::]) && input.peek3(Token![<]) {
    ///             return Ok(PathSegment {
    ///                 ident,
    ///                 arguments: PathArguments::AngleBracketed(input.parse()?),
    ///             });
    ///         }
    ///         if input.peek(Token![<]) && !input.peek(Token![<=]) {
    ///             let fork = input.fork();
    ///             if let Ok(arguments) = fork.parse() {
    ///                 input.advance_to(&fork);
    ///                 return Ok(PathSegment {
    ///                     ident,
    ///                     arguments: PathArguments::AngleBracketed(arguments),
    ///                 });
    ///             }
    ///         }
    ///         Ok(PathSegment::from(ident))
    ///     }
    /// }
    ///
    /// # syn::parse_str::<PathSegment>("a<b,c>").unwrap();
    /// ```
    ///
    /// # Drawbacks
    ///
    /// The main drawback of this style of speculative parsing is in error
    /// presentation. Even if the lookahead is the "correct" parse, the error
    /// that is shown is that of the "fallback" parse. To use the same example
    /// as the turbofish above, take the following unfinished "turbofish":
    ///
    /// ```text
    /// let _ = f<&'a fn(), for<'a> serde::>();
    /// ```
    ///
    /// If this is parsed as generic arguments, we can provide the error message
    ///
    /// ```text
    /// error: expected identifier
    ///  --> src.rs:L:C
    ///   |
    /// L | let _ = f<&'a fn(), for<'a> serde::>();
    ///   |                                    ^
    /// ```
    ///
    /// but if parsed using the above speculative parsing, it falls back to
    /// assuming that the `<` is a less-than when it fails to parse the generic
    /// arguments, and tries to interpret the `&'a` as the start of a labelled
    /// loop, resulting in the much less helpful error
    ///
    /// ```text
    /// error: expected `:`
    ///  --> src.rs:L:C
    ///   |
    /// L | let _ = f<&'a fn(), for<'a> serde::>();
    ///   |               ^^
    /// ```
    ///
    /// This can be mitigated with various heuristics (two examples: show both
    /// forks' parse errors, or show the one that consumed more tokens), but
    /// when you can control the grammar, sticking to something that can be
    /// parsed LL(3) and without the LL(*) speculative parsing this makes
    /// possible, displaying reasonable errors becomes much more simple.
    ///
    /// [RFC 2544]: https://github.com/rust-lang/rfcs/pull/2544
    /// [`PathSegment`]: crate::PathSegment
    ///
    /// # Performance
    ///
    /// This method performs a cheap fixed amount of work that does not depend
    /// on how far apart the two streams are positioned.
    ///
    /// # Panics
    ///
    /// The forked stream in the argument of `advance_to` must have been
    /// obtained by forking `self`. Attempting to advance to any other stream
    /// will cause a panic.
    fn advance_to(&self, fork: &Self);
}

impl<'a> Speculative for ParseBuffer<'a> {
    fn advance_to(&self, fork: &Self) {
        if !crate::buffer::same_scope(self.cursor(), fork.cursor()) {
            panic!("Fork was not derived from the advancing parse stream");
        }

        let (self_unexp, self_sp) = inner_unexpected(self);
        let (fork_unexp, fork_sp) = inner_unexpected(fork);
        if !Rc::ptr_eq(&self_unexp, &fork_unexp) {
            match (fork_sp, self_sp) {
                // Unexpected set on the fork, but not on `self`, copy it over.
                (Some(span), None) => {
                    self_unexp.set(Unexpected::Some(span));
                }
                // Unexpected unset. Use chain to propagate errors from fork.
                (None, None) => {
                    fork_unexp.set(Unexpected::Chain(self_unexp));

                    // Ensure toplevel 'unexpected' tokens from the fork don't
                    // bubble up the chain by replacing the root `unexpected`
                    // pointer, only 'unexpected' tokens from existing group
                    // parsers should bubble.
                    fork.unexpected
                        .set(Some(Rc::new(Cell::new(Unexpected::None))));
                }
                // Unexpected has been set on `self`. No changes needed.
                (_, Some(_)) => {}
            }
        }

        // See comment on `cell` in the struct definition.
        self.cell
            .set(unsafe { mem::transmute::<Cursor, Cursor<'static>>(fork.cursor()) })
    }
}
<|endoftext|>


<commit_before>use gettextrs::gettext;
use gstreamer as gst;

use log::error;

use std::{io::Write, string::ToString};

use super::{get_default_chapter_title, MediaInfo, Timestamp4Humans, TocVisitor, Writer};

static EXTENSION: &str = "cue";

pub struct CueSheetFormat {}

impl CueSheetFormat {
    pub fn get_extension() -> &'static str {
        EXTENSION
    }

    pub fn new_as_boxed() -> Box<Self> {
        Box::new(CueSheetFormat {})
    }
}

macro_rules! write_fmt(
    ($dest:ident, $fmt:expr, $( $item:expr ),*) => {
        $dest.write_fmt(format_args!($fmt, $( $item ),*)).map_err(|_| {
            let msg = gettext("Failed to write Cue Sheet file");
            error!("{}", msg);
            msg
        })?;
    };
);

impl Writer for CueSheetFormat {
    fn write(&self, info: &MediaInfo, destination: &mut dyn Write) -> Result<(), String> {
        if info.toc.is_none() {
            let msg = gettext("The table of contents is empty");
            error!("{}", msg);
            return Err(msg);
        }

        let media_title = info.get_media_title();
        if let Some(title) = &media_title {
            write_fmt!(destination, "TITLE \"{}\"\n", title);
        }

        let media_artist = info.get_media_artist();
        if let Some(artist) = &media_artist {
            write_fmt!(destination, "PERFORMER \"{}\"\n", artist);
        }

        let audio_codec = info
            .streams
            .get_audio_codec()
            .map_or("WAVE", |audio_codec| {
                if audio_codec.to_lowercase().find("mp3").is_some() {
                    "MP3"
                } else if audio_codec.to_lowercase().find("aiff").is_some() {
                    "AIFF"
                } else {
                    "WAVE"
                }
            });
        write_fmt!(
            destination,
            "FILE \"{}\" {}\n",
            info.get_file_name(),
            audio_codec
        );

        let mut index = 0;
        let mut toc_visitor = TocVisitor::new(info.toc.as_ref().unwrap());
        while let Some(chapter) = toc_visitor.next_chapter() {
            index += 1;
            // FIXME: are there other TRACK types than AUDIO?
            write_fmt!(destination, "  TRACK{:02} AUDIO\n", index);

            let title = chapter
                .get_tags()
                .and_then(|tags| {
                    tags.get::<gst::tags::Title>()
                        .and_then(|value| value.get().map(ToString::to_string))
                })
                .or_else(|| media_title.clone())
                .unwrap_or_else(get_default_chapter_title);
            write_fmt!(destination, "    TITLE \"{}\"\n", &title);

            let artist = chapter
                .get_tags()
                .and_then(|tags| {
                    tags.get::<gst::tags::Artist>()
                        .and_then(|value| value.get().map(ToString::to_string))
                })
                .or_else(|| media_artist.clone())
                .unwrap_or_else(get_default_chapter_title);
            write_fmt!(destination, "    PERFORMER \"{}\"\n", &artist);

            if let Some((start, _end)) = chapter.get_start_stop_times() {
                let start_ts = Timestamp4Humans::from_nano(start as u64);
                write_fmt!(
                    destination,
                    "    INDEX 01 {:02}:{:02}:{:02}\n",
                    start_ts.h * 60 + start_ts.m,
                    start_ts.s,
                    (f64::from((start_ts.ms * 1_000 + start_ts.us) * 1_000 + start_ts.nano)
                        / 1_000_000_000f64
                        * 75f64)
                        .round() // frame nb (75 frames/s for Cue Sheets)
                );
            }
        }

        Ok(())
    }
}
<commit_msg>Fix overflow exporting to CUE sheet<commit_after>use gettextrs::gettext;
use gstreamer as gst;

use log::error;

use std::{io::Write, string::ToString};

use super::{get_default_chapter_title, MediaInfo, Timestamp4Humans, TocVisitor, Writer};

static EXTENSION: &str = "cue";

pub struct CueSheetFormat {}

impl CueSheetFormat {
    pub fn get_extension() -> &'static str {
        EXTENSION
    }

    pub fn new_as_boxed() -> Box<Self> {
        Box::new(CueSheetFormat {})
    }
}

macro_rules! write_fmt(
    ($dest:ident, $fmt:expr, $( $item:expr ),*) => {
        $dest.write_fmt(format_args!($fmt, $( $item ),*)).map_err(|_| {
            let msg = gettext("Failed to write Cue Sheet file");
            error!("{}", msg);
            msg
        })?;
    };
);

impl Writer for CueSheetFormat {
    fn write(&self, info: &MediaInfo, destination: &mut dyn Write) -> Result<(), String> {
        if info.toc.is_none() {
            let msg = gettext("The table of contents is empty");
            error!("{}", msg);
            return Err(msg);
        }

        let media_title = info.get_media_title();
        if let Some(title) = &media_title {
            write_fmt!(destination, "TITLE \"{}\"\n", title);
        }

        let media_artist = info.get_media_artist();
        if let Some(artist) = &media_artist {
            write_fmt!(destination, "PERFORMER \"{}\"\n", artist);
        }

        let audio_codec = info
            .streams
            .get_audio_codec()
            .map_or("WAVE", |audio_codec| {
                if audio_codec.to_lowercase().find("mp3").is_some() {
                    "MP3"
                } else if audio_codec.to_lowercase().find("aiff").is_some() {
                    "AIFF"
                } else {
                    "WAVE"
                }
            });
        write_fmt!(
            destination,
            "FILE \"{}\" {}\n",
            info.get_file_name(),
            audio_codec
        );

        let mut index = 0;
        let mut toc_visitor = TocVisitor::new(info.toc.as_ref().unwrap());
        while let Some(chapter) = toc_visitor.next_chapter() {
            index += 1;
            // FIXME: are there other TRACK types than AUDIO?
            write_fmt!(destination, "  TRACK{:02} AUDIO\n", index);

            let title = chapter
                .get_tags()
                .and_then(|tags| {
                    tags.get::<gst::tags::Title>()
                        .and_then(|value| value.get().map(ToString::to_string))
                })
                .or_else(|| media_title.clone())
                .unwrap_or_else(get_default_chapter_title);
            write_fmt!(destination, "    TITLE \"{}\"\n", &title);

            let artist = chapter
                .get_tags()
                .and_then(|tags| {
                    tags.get::<gst::tags::Artist>()
                        .and_then(|value| value.get().map(ToString::to_string))
                })
                .or_else(|| media_artist.clone())
                .unwrap_or_else(get_default_chapter_title);
            write_fmt!(destination, "    PERFORMER \"{}\"\n", &artist);

            if let Some((start, _end)) = chapter.get_start_stop_times() {
                let start_ts = Timestamp4Humans::from_nano(start as u64);
                write_fmt!(
                    destination,
                    "    INDEX 01 {:02}:{:02}:{:02}\n",
                    start_ts.h * 60 + start_ts.m,
                    start_ts.s,
                    (f64::from(
                        (start_ts.ms as u32 * 1_000 + start_ts.us as u32) * 1_000
                            + start_ts.nano as u32
                    ) / 1_000_000_000f64
                        * 75f64)
                        .round() // frame nb (75 frames/s for Cue Sheets)
                );
            }
        }

        Ok(())
    }
}
<|endoftext|>


<commit_before>// Copyright 2015 Nathan Sizemore <nathanrsizemore@gmail.com>
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL was not
// distributed with this file, You can obtain one at
// http://mozilla.org/MPL/2.0/.

use std::mem;
use std::os::unix::io::{RawFd, AsRawFd};
use std::io::{Read, Write, Error, ErrorKind};

use libc;
use errno::errno;
use libc::{c_int, c_void};

#[derive(Clone)]
pub struct Socket {
    /// This socket's file descriptor
    pub fd: c_int,
}

impl Socket {

    pub fn set_tcp_keepalive(&self, keepalive: bool) {
        let optval: c_int = match keepalive {
            true => 1,
            false => 0
        };
        let opt_result = libc::setsockopt(self.fd,
                                          libc::SOL_SOCKET,
                                          libc::SO_KEEPALIVE,
                                          &optval as *const _ as *const c_void,
                                          mem::size_of::<c_int>() as u32);
        if opt_result < 0 {
            error!("Setting SO_KEEPALIVE: {}",
                   Error::from_raw_os_error(errno().0 as i32));
        }
    }

    pub fn set_tcp_nodelay(&self, nodelay: bool) {
        let optval: c_int = match nodelay {
            true => 1,
            false => 0
        };
        let opt_result = libc::setsockopt(self.fd,
                                          libc::SOL_TCP,
                                          libc::SO_KEEPALIVE,
                                          &optval as *const _ as *const c_void,
                                          mem::size_of::<c_int>() as u32);
        if opt_result < 0 {
            error!("Setting TCP_NODELAY: {}",
                   Error::from_raw_os_error(errno().0 as i32));
        }
    }
}

impl Read for Socket {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        if buf.len() < 1 {
            return Err(Error::new(ErrorKind::Other, "Invalid buffer"));
        }

        let result = unsafe { libc::read(self.fd, buf as *mut _ as *mut c_void, buf.len()) };

        if result < 0 {
            return Err(Error::from_raw_os_error(errno().0 as i32));
        }

        if result == 0 {
            return Err(Error::new(ErrorKind::Other, "EOF"));
        }

        Ok(result as usize)
    }
}

impl Write for Socket {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
        let result = unsafe { libc::write(self.fd, buf as *const _ as *const c_void, buf.len()) };

        if result < 0 {
            return Err(Error::from_raw_os_error(errno().0 as i32));
        }

        Ok(result as usize)
    }

    fn flush(&mut self) -> Result<(), Error> {
        Ok(())
    }
}

impl AsRawFd for Socket {
    fn as_raw_fd(&self) -> RawFd {
        self.fd
    }
}
<commit_msg>Remote debugging<commit_after>// Copyright 2015 Nathan Sizemore <nathanrsizemore@gmail.com>
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL was not
// distributed with this file, You can obtain one at
// http://mozilla.org/MPL/2.0/.

use std::mem;
use std::os::unix::io::{RawFd, AsRawFd};
use std::io::{Read, Write, Error, ErrorKind};

use libc;
use errno::errno;
use libc::{c_int, c_void};

#[derive(Clone)]
pub struct Socket {
    /// This socket's file descriptor
    pub fd: c_int,
}

impl Socket {

    pub fn set_tcp_keepalive(&self, keepalive: bool) {
        let optval: c_int = match keepalive {
            true => 1,
            false => 0
        };
        let opt_result = libc::setsockopt(self.fd,
                                          libc::SOL_SOCKET,
                                          libc::SO_KEEPALIVE,
                                          &optval as *const _ as *const c_void,
                                          mem::size_of::<c_int>() as u32);
        if opt_result < 0 {
            error!("Setting SO_KEEPALIVE: {}",
                   Error::from_raw_os_error(errno().0 as i32));
        }
    }

    pub fn set_tcp_nodelay(&self, nodelay: bool) {
        const SOL_TCP: c_int = 6;

        let optval: c_int = match nodelay {
            true => 1,
            false => 0
        };
        let opt_result = libc::setsockopt(self.fd,
                                          libc::SOL_TCP,
                                          libc::SO_KEEPALIVE,
                                          &optval as *const _ as *const c_void,
                                          mem::size_of::<c_int>() as u32);
        if opt_result < 0 {
            error!("Setting TCP_NODELAY: {}",
                   Error::from_raw_os_error(errno().0 as i32));
        }
    }
}

impl Read for Socket {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        if buf.len() < 1 {
            return Err(Error::new(ErrorKind::Other, "Invalid buffer"));
        }

        let result = unsafe { libc::read(self.fd, buf as *mut _ as *mut c_void, buf.len()) };

        if result < 0 {
            return Err(Error::from_raw_os_error(errno().0 as i32));
        }

        if result == 0 {
            return Err(Error::new(ErrorKind::Other, "EOF"));
        }

        Ok(result as usize)
    }
}

impl Write for Socket {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
        let result = unsafe { libc::write(self.fd, buf as *const _ as *const c_void, buf.len()) };

        if result < 0 {
            return Err(Error::from_raw_os_error(errno().0 as i32));
        }

        Ok(result as usize)
    }

    fn flush(&mut self) -> Result<(), Error> {
        Ok(())
    }
}

impl AsRawFd for Socket {
    fn as_raw_fd(&self) -> RawFd {
        self.fd
    }
}
<|endoftext|>


<commit_before>use std::{
    path::{Path, PathBuf},
    thread::sleep,
    time::Duration,
};

use isahc::{
    auth::{Authentication, Credentials},
    config::Configurable,
    http::StatusCode,
    HttpClient, ReadResponseExt, Request,
};
use itertools::Itertools;
use semver::Version;
use serde::{de::IgnoredAny, Deserialize};
use serde_json::json;
use toml::from_str as from_toml_str;
use xshell::{pushd, read_file};

use crate::{cmd, util::ask_yes_no, GithubConfig, Result};

const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma";

/// Task to create a new release of the given crate.
#[derive(Debug)]
pub struct ReleaseTask {
    /// The crate to release.
    local_crate: LocalCrate,

    /// The root of the workspace.
    project_root: PathBuf,

    /// The http client to use for requests.
    http_client: HttpClient,

    /// The github configuration required to publish a release.
    config: GithubConfig,
}

impl ReleaseTask {
    /// Create a new `ReleaseTask` with the given `name` and `project_root`.
    pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> {
        let local_crate = LocalCrate::new(name, &project_root)?;
        let config = crate::config()?.github;
        let http_client = HttpClient::new()?;

        Ok(Self { local_crate, project_root, http_client, config })
    }

    /// Run the task to effectively create a release.
    pub(crate) fn run(self) -> Result<()> {
        let title = &self.title();
        let prerelease = self.local_crate.version.is_prerelease();
        println!(
            "Starting {} for {}…",
            match prerelease {
                true => "pre-release",
                false => "release",
            },
            title
        );

        if self.is_released()? {
            return Err("This crate version is already released".into());
        }

        let remote = Self::git_remote()?;

        println!("Checking status of git repository…");
        if !cmd!("git status -s -uno").read()?.is_empty()
            && !ask_yes_no("This git repository contains untracked files. Continue?")?
        {
            return Ok(());
        }

        if let Some(macros) = self.macros() {
            print!("Found macros crate. ");
            let _dir = pushd(&macros.path)?;
            macros.publish(&self.http_client)?;

            println!("Waiting 10 seconds for the release to make it into the crates.io index…");
            sleep(Duration::from_secs(10));

            println!("Resuming release of {}…", self.title());
        }

        let _dir = pushd(&self.local_crate.path)?;

        self.local_crate.publish(&self.http_client)?;

        if prerelease {
            println!("Pre-release created successfully!");
            return Ok(());
        }

        let changes = &self.local_crate.changes()?;

        let tag = &self.tag_name();

        println!("Creating git tag…");
        if cmd!("git tag -l {tag}").read()?.is_empty() {
            cmd!("git tag -s {tag} -m {title} -m {changes}").read()?;
        } else if !ask_yes_no("This tag already exists. Skip this step and continue?")? {
            return Ok(());
        }

        println!("Pushing tag to remote repository…");
        if cmd!("git ls-remote --tags {remote} {tag}").read()?.is_empty() {
            cmd!("git push {remote} {tag}").run()?;
        } else if !ask_yes_no("This tag has already been pushed. Skip this step and continue?")? {
            return Ok(());
        }

        println!("Creating release on GitHub…");
        let request_body = &json!({
            "tag_name": tag,
            "name": title,
            "body": changes.trim_softbreaks(),
        })
        .to_string();

        self.release(request_body)?;

        println!("Release created successfully!");

        Ok(())
    }

    /// Get the associated `-macros` crate of the current crate, if any.
    fn macros(&self) -> Option<LocalCrate> {
        LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
    }

    /// Get the title of this release.
    fn title(&self) -> String {
        format!("{} {}", self.local_crate.name, self.local_crate.version)
    }

    /// Load the GitHub config from the config file.
    fn git_remote() -> Result<String> {
        let branch = cmd!("git rev-parse --abbrev-ref HEAD").read()?;
        let remote = cmd!("git config branch.{branch}.remote").read()?;

        if remote.is_empty() {
            return Err("Could not get current git remote".into());
        }

        Ok(remote)
    }

    /// Get the tag name for this release.
    fn tag_name(&self) -> String {
        format!("{}-{}", self.local_crate.name, self.local_crate.version)
    }

    /// Check if the tag for the current version of the crate has been pushed on GitHub.
    fn is_released(&self) -> Result<bool> {
        let response = self.http_client.get(format!(
            "{}/releases/tags/{}",
            GITHUB_API_RUMA,
            self.tag_name()
        ))?;

        Ok(response.status() == StatusCode::OK)
    }

    /// Create the release on GitHub with the given `config` and `credentials`.
    fn release(&self, body: &str) -> Result<()> {
        let request = Request::post(format!("{}/releases", GITHUB_API_RUMA))
            .authentication(Authentication::basic())
            .credentials(Credentials::new(&self.config.user, &self.config.token))
            .header("Accept", "application/vnd.github.v3+json")
            .body(body)?;

        let mut response = self.http_client.send(request)?;

        if response.status() == StatusCode::CREATED {
            Ok(())
        } else {
            Err(format!("{}: {}", response.status(), response.text()?).into())
        }
    }
}

/// A local Rust crate.
#[derive(Debug)]
struct LocalCrate {
    /// The name of the crate.
    name: String,

    /// The version of the crate.
    version: Version,

    /// The local path of the crate.
    path: PathBuf,
}

impl LocalCrate {
    /// Creates a new `Crate` with the given name and project root.
    pub fn new(name: String, project_root: &Path) -> Result<Self> {
        let path = project_root.join(&name);

        let version = Self::version(&path)?;

        Ok(Self { name, version, path })
    }

    /// The current version of the crate at `path` from its manifest.
    fn version(path: &Path) -> Result<Version> {
        let manifest_toml = read_file(path.join("Cargo.toml"))?;
        let manifest: CargoManifest = from_toml_str(&manifest_toml)?;

        Ok(manifest.package.version)
    }

    /// The changes of the given version from the changelog.
    fn changes(&self) -> Result<String> {
        let changelog = read_file(self.path.join("CHANGELOG.md"))?;
        let lines_nb = changelog.lines().count();
        let mut lines = changelog.lines();

        let start = match lines.position(|l| l.starts_with(&format!("# {}", self.version))) {
            Some(p) => p + 1,
            None => {
                return Err("Could not find version title in changelog".into());
            }
        };

        let length = match lines.position(|l| l.starts_with("# ")) {
            Some(p) => p,
            None => lines_nb,
        };

        let changes = changelog.lines().skip(start).take(length).join("\n");

        Ok(changes.trim().to_owned())
    }

    /// Check if the current version of the crate is published on crates.io.
    fn is_published(&self, client: &HttpClient) -> Result<bool> {
        let response: CratesIoCrate =
            client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;

        Ok(response.version.is_some())
    }

    /// Publish this package on crates.io.
    fn publish(&self, client: &HttpClient) -> Result<()> {
        println!("Publishing {} {} on crates.io…", self.name, self.version);
        if self.is_published(client)? {
            if ask_yes_no("This version is already published. Skip this step and continue?")? {
                Ok(())
            } else {
                Err("Release interrupted by user.".into())
            }
        } else {
            Ok(cmd!("cargo publish").run()?)
        }
    }
}

/// The required cargo manifest data of a crate.
#[derive(Debug, Deserialize)]
struct CargoManifest {
    /// The package information.
    package: CargoPackage,
}

/// The required package information from a crate's cargo manifest.
#[derive(Debug, Deserialize)]
struct CargoPackage {
    /// The package version.
    version: Version,
}

/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
#[derive(Deserialize)]
struct CratesIoCrate {
    version: Option<IgnoredAny>,
}

/// A tag from the `GET /repos/{owner}/{repo}/tags` endpoint of GitHub REST API.
#[derive(Debug, Deserialize)]
struct GithubTag {
    /// The name of the tag.
    name: String,
}

/// String manipulations for crate release.
trait StrExt {
    /// Remove soft line breaks as defined in CommonMark spec.
    fn trim_softbreaks(&self) -> String;
}

impl StrExt for str {
    fn trim_softbreaks(&self) -> String {
        let mut string = String::new();
        let mut s = self;

        while let Some(pos) = s.find('\n') {
            string.push_str(&s[..pos]);
            let pos_s = &s[pos..];

            if pos_s.starts_with("\n\n") {
                // Keep new paragraphs (multiple `\n`s).
                let next = pos_s.find(|c: char| c != '\n').unwrap_or(0);
                let (push, next_s) = pos_s.split_at(next);

                string.push_str(push);
                s = next_s;
            } else if s[..pos].ends_with("  ") || s[..pos].ends_with('\\') {
                // Keep hard line breaks (two spaces or a backslash before the line break).
                string.push('\n');
                s = &pos_s[1..];
            } else if let Some(p) = pos_s.find(|c: char| !c.is_ascii_whitespace()) {
                // Keep line break before list items (`\n` + whitespaces + `*` + whitespaces).
                // Remove line break and keep one space otherwise.
                let mut chars = pos_s.char_indices();
                let (_, char) = chars.find(|(i, _)| *i == p).unwrap();

                if char == '*' || char == '-' {
                    match chars.next() {
                        Some((_, next_char)) if next_char.is_ascii_whitespace() => {
                            string.push('\n');
                            s = &pos_s[1..];
                            continue;
                        }
                        _ => {}
                    }
                }

                string.push(' ');
                s = &pos_s[p..];
            }
        }

        string + s
    }
}
<commit_msg>xtask: Skip 10sec delay after publishing macro crate was skipped<commit_after>use std::{
    path::{Path, PathBuf},
    thread::sleep,
    time::Duration,
};

use isahc::{
    auth::{Authentication, Credentials},
    config::Configurable,
    http::StatusCode,
    HttpClient, ReadResponseExt, Request,
};
use itertools::Itertools;
use semver::Version;
use serde::{de::IgnoredAny, Deserialize};
use serde_json::json;
use toml::from_str as from_toml_str;
use xshell::{pushd, read_file};

use crate::{cmd, util::ask_yes_no, GithubConfig, Result};

const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma";

/// Task to create a new release of the given crate.
#[derive(Debug)]
pub struct ReleaseTask {
    /// The crate to release.
    local_crate: LocalCrate,

    /// The root of the workspace.
    project_root: PathBuf,

    /// The http client to use for requests.
    http_client: HttpClient,

    /// The github configuration required to publish a release.
    config: GithubConfig,
}

impl ReleaseTask {
    /// Create a new `ReleaseTask` with the given `name` and `project_root`.
    pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> {
        let local_crate = LocalCrate::new(name, &project_root)?;
        let config = crate::config()?.github;
        let http_client = HttpClient::new()?;

        Ok(Self { local_crate, project_root, http_client, config })
    }

    /// Run the task to effectively create a release.
    pub(crate) fn run(self) -> Result<()> {
        let title = &self.title();
        let prerelease = self.local_crate.version.is_prerelease();
        println!(
            "Starting {} for {}…",
            match prerelease {
                true => "pre-release",
                false => "release",
            },
            title
        );

        if self.is_released()? {
            return Err("This crate version is already released".into());
        }

        let remote = Self::git_remote()?;

        println!("Checking status of git repository…");
        if !cmd!("git status -s -uno").read()?.is_empty()
            && !ask_yes_no("This git repository contains untracked files. Continue?")?
        {
            return Ok(());
        }

        if let Some(macros) = self.macros() {
            print!("Found macros crate. ");
            let _dir = pushd(&macros.path)?;
            let published = macros.publish(&self.http_client)?;

            if published {
                // Crate was published, instead of publishing skipped (because release already
                // existed).
                println!("Waiting 10 seconds for the release to make it into the crates.io index…");
                sleep(Duration::from_secs(10));
            }

            println!("Resuming release of {}…", self.title());
        }

        let _dir = pushd(&self.local_crate.path)?;

        self.local_crate.publish(&self.http_client)?;

        if prerelease {
            println!("Pre-release created successfully!");
            return Ok(());
        }

        let changes = &self.local_crate.changes()?;

        let tag = &self.tag_name();

        println!("Creating git tag…");
        if cmd!("git tag -l {tag}").read()?.is_empty() {
            cmd!("git tag -s {tag} -m {title} -m {changes}").read()?;
        } else if !ask_yes_no("This tag already exists. Skip this step and continue?")? {
            return Ok(());
        }

        println!("Pushing tag to remote repository…");
        if cmd!("git ls-remote --tags {remote} {tag}").read()?.is_empty() {
            cmd!("git push {remote} {tag}").run()?;
        } else if !ask_yes_no("This tag has already been pushed. Skip this step and continue?")? {
            return Ok(());
        }

        println!("Creating release on GitHub…");
        let request_body = &json!({
            "tag_name": tag,
            "name": title,
            "body": changes.trim_softbreaks(),
        })
        .to_string();

        self.release(request_body)?;

        println!("Release created successfully!");

        Ok(())
    }

    /// Get the associated `-macros` crate of the current crate, if any.
    fn macros(&self) -> Option<LocalCrate> {
        LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
    }

    /// Get the title of this release.
    fn title(&self) -> String {
        format!("{} {}", self.local_crate.name, self.local_crate.version)
    }

    /// Load the GitHub config from the config file.
    fn git_remote() -> Result<String> {
        let branch = cmd!("git rev-parse --abbrev-ref HEAD").read()?;
        let remote = cmd!("git config branch.{branch}.remote").read()?;

        if remote.is_empty() {
            return Err("Could not get current git remote".into());
        }

        Ok(remote)
    }

    /// Get the tag name for this release.
    fn tag_name(&self) -> String {
        format!("{}-{}", self.local_crate.name, self.local_crate.version)
    }

    /// Check if the tag for the current version of the crate has been pushed on GitHub.
    fn is_released(&self) -> Result<bool> {
        let response = self.http_client.get(format!(
            "{}/releases/tags/{}",
            GITHUB_API_RUMA,
            self.tag_name()
        ))?;

        Ok(response.status() == StatusCode::OK)
    }

    /// Create the release on GitHub with the given `config` and `credentials`.
    fn release(&self, body: &str) -> Result<()> {
        let request = Request::post(format!("{}/releases", GITHUB_API_RUMA))
            .authentication(Authentication::basic())
            .credentials(Credentials::new(&self.config.user, &self.config.token))
            .header("Accept", "application/vnd.github.v3+json")
            .body(body)?;

        let mut response = self.http_client.send(request)?;

        if response.status() == StatusCode::CREATED {
            Ok(())
        } else {
            Err(format!("{}: {}", response.status(), response.text()?).into())
        }
    }
}

/// A local Rust crate.
#[derive(Debug)]
struct LocalCrate {
    /// The name of the crate.
    name: String,

    /// The version of the crate.
    version: Version,

    /// The local path of the crate.
    path: PathBuf,
}

impl LocalCrate {
    /// Creates a new `Crate` with the given name and project root.
    pub fn new(name: String, project_root: &Path) -> Result<Self> {
        let path = project_root.join(&name);

        let version = Self::version(&path)?;

        Ok(Self { name, version, path })
    }

    /// The current version of the crate at `path` from its manifest.
    fn version(path: &Path) -> Result<Version> {
        let manifest_toml = read_file(path.join("Cargo.toml"))?;
        let manifest: CargoManifest = from_toml_str(&manifest_toml)?;

        Ok(manifest.package.version)
    }

    /// The changes of the given version from the changelog.
    fn changes(&self) -> Result<String> {
        let changelog = read_file(self.path.join("CHANGELOG.md"))?;
        let lines_nb = changelog.lines().count();
        let mut lines = changelog.lines();

        let start = match lines.position(|l| l.starts_with(&format!("# {}", self.version))) {
            Some(p) => p + 1,
            None => {
                return Err("Could not find version title in changelog".into());
            }
        };

        let length = match lines.position(|l| l.starts_with("# ")) {
            Some(p) => p,
            None => lines_nb,
        };

        let changes = changelog.lines().skip(start).take(length).join("\n");

        Ok(changes.trim().to_owned())
    }

    /// Check if the current version of the crate is published on crates.io.
    fn is_published(&self, client: &HttpClient) -> Result<bool> {
        let response: CratesIoCrate =
            client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;

        Ok(response.version.is_some())
    }

    /// Publish this package on crates.io.
    fn publish(&self, client: &HttpClient) -> Result<bool> {
        println!("Publishing {} {} on crates.io…", self.name, self.version);
        if self.is_published(client)? {
            if ask_yes_no("This version is already published. Skip this step and continue?")? {
                Ok(false)
            } else {
                Err("Release interrupted by user.".into())
            }
        } else {
            cmd!("cargo publish").run()?;
            Ok(true)
        }
    }
}

/// The required cargo manifest data of a crate.
#[derive(Debug, Deserialize)]
struct CargoManifest {
    /// The package information.
    package: CargoPackage,
}

/// The required package information from a crate's cargo manifest.
#[derive(Debug, Deserialize)]
struct CargoPackage {
    /// The package version.
    version: Version,
}

/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
#[derive(Deserialize)]
struct CratesIoCrate {
    version: Option<IgnoredAny>,
}

/// A tag from the `GET /repos/{owner}/{repo}/tags` endpoint of GitHub REST API.
#[derive(Debug, Deserialize)]
struct GithubTag {
    /// The name of the tag.
    name: String,
}

/// String manipulations for crate release.
trait StrExt {
    /// Remove soft line breaks as defined in CommonMark spec.
    fn trim_softbreaks(&self) -> String;
}

impl StrExt for str {
    fn trim_softbreaks(&self) -> String {
        let mut string = String::new();
        let mut s = self;

        while let Some(pos) = s.find('\n') {
            string.push_str(&s[..pos]);
            let pos_s = &s[pos..];

            if pos_s.starts_with("\n\n") {
                // Keep new paragraphs (multiple `\n`s).
                let next = pos_s.find(|c: char| c != '\n').unwrap_or(0);
                let (push, next_s) = pos_s.split_at(next);

                string.push_str(push);
                s = next_s;
            } else if s[..pos].ends_with("  ") || s[..pos].ends_with('\\') {
                // Keep hard line breaks (two spaces or a backslash before the line break).
                string.push('\n');
                s = &pos_s[1..];
            } else if let Some(p) = pos_s.find(|c: char| !c.is_ascii_whitespace()) {
                // Keep line break before list items (`\n` + whitespaces + `*` + whitespaces).
                // Remove line break and keep one space otherwise.
                let mut chars = pos_s.char_indices();
                let (_, char) = chars.find(|(i, _)| *i == p).unwrap();

                if char == '*' || char == '-' {
                    match chars.next() {
                        Some((_, next_char)) if next_char.is_ascii_whitespace() => {
                            string.push('\n');
                            s = &pos_s[1..];
                            continue;
                        }
                        _ => {}
                    }
                }

                string.push(' ');
                s = &pos_s[p..];
            }
        }

        string + s
    }
}
<|endoftext|>


<commit_before>//! Parser for an Accept-Language HTTP header.

use language_tags::{LanguageTag, ParseError};
use locale_config::{LanguageRange, Locale};

use std::error;
use std::fmt;
use std::str::FromStr;

/// Used to set the language for rendering.
///
/// SVG documents can use the `<switch>` element, whose children have a `systemLanguage`
/// attribute; only the first child which has a `systemLanguage` that matches the
/// preferred languages will be rendered.
///
/// This enum, used with `CairoRenderer::with_language`, configures how to obtain the
/// user's prefererred languages.
pub enum Language {
    /// Use the Unix environment variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` to obtain the
    /// user's language.  This uses [`g_get_language_names()`][ggln] underneath.
    ///
    /// [ggln]: https://developer.gnome.org/glib/stable/glib-I18N.html#g-get-language-names
    FromEnvironment,
    AcceptLanguage(AcceptLanguage),
}

/// `Language` but with the environment's locale converted to something we can use.
#[derive(Clone)]
pub enum UserLanguage {
    LanguageTags(LanguageTags),
    AcceptLanguage(AcceptLanguage),
}

#[derive(Clone, Debug, PartialEq)]
struct Weight(Option<f32>);

impl Weight {
    fn numeric(&self) -> f32 {
        self.0.unwrap_or(1.0)
    }
}

#[derive(Clone, Debug, PartialEq)]
struct Item {
    tag: LanguageTag,
    weight: Weight,
}

/// Stores a parsed version of an HTTP Accept-Language header.
///
/// https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5
#[derive(Clone, Debug, PartialEq)]
pub struct AcceptLanguage(Box<[Item]>);

/// Errors when parsing an `AcceptLanguage`.
#[derive(Debug, PartialEq)]
pub enum AcceptLanguageError {
    NoElements,
    InvalidCharacters,
    InvalidLanguageTag(ParseError),
    InvalidWeight,
}

impl error::Error for AcceptLanguageError {}

impl fmt::Display for AcceptLanguageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NoElements => write!(f, "no language tags in list"),
            Self::InvalidCharacters => write!(f, "invalid characters in language list"),
            Self::InvalidLanguageTag(e) => write!(f, "invalid language tag: {}", e),
            Self::InvalidWeight => write!(f, "invalid q= weight"),
        }
    }
}

/// Optional whitespace, Space or Tab, per https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
const OWS: [char; 2] = ['\x20', '\x09'];

impl AcceptLanguage {
    pub fn parse(s: &str) -> Result<AcceptLanguage, AcceptLanguageError> {
        if !s.is_ascii() {
            return Err(AcceptLanguageError::InvalidCharacters);
        }

        let mut items = Vec::new();

        for val in s.split(',') {
            let trimmed = val.trim_matches(&OWS[..]);
            if trimmed.is_empty() {
                continue;
            }

            items.push(Item::parse(trimmed)?);
        }

        if items.is_empty() {
            Err(AcceptLanguageError::NoElements)
        } else {
            Ok(AcceptLanguage(items.into_boxed_slice()))
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = (&LanguageTag, f32)> {
        self.0.iter().map(|item| (&item.tag, item.weight.numeric()))
    }

    fn any_matches(&self, tag: &LanguageTag) -> bool {
        self.iter().any(|(self_tag, _weight)| tag.matches(self_tag))
    }
}

impl Item {
    fn parse(s: &str) -> Result<Item, AcceptLanguageError> {
        let semicolon_pos = s.find(';');

        let (before_semicolon, after_semicolon) = if let Some(semi) = semicolon_pos {
            (&s[..semi], Some(&s[semi + 1..]))
        } else {
            (s, None)
        };

        let tag = LanguageTag::parse(before_semicolon)
            .map_err(AcceptLanguageError::InvalidLanguageTag)?;

        let weight;

        if let Some(quality) = after_semicolon {
            let quality = quality.trim_start_matches(&OWS[..]);

            let number = if let Some(qvalue) = quality.strip_prefix("q=") {
                if qvalue.starts_with(&['0', '1'][..]) {
                    let first_digit = qvalue.chars().next().unwrap();

                    if let Some(decimals) = qvalue[1..].strip_prefix(".") {
                        if first_digit == '0'
                            && decimals.len() <= 3
                            && decimals.chars().all(|c| c.is_digit(10))
                        {
                            qvalue
                        } else if first_digit == '1'
                            && decimals.len() <= 3
                            && decimals.chars().all(|c| c == '0')
                        {
                            qvalue
                        } else {
                            return Err(AcceptLanguageError::InvalidWeight);
                        }
                    } else {
                        qvalue
                    }
                } else {
                    return Err(AcceptLanguageError::InvalidWeight);
                }
            } else {
                return Err(AcceptLanguageError::InvalidWeight);
            };

            weight = Weight(Some(
                f32::from_str(number).map_err(|_| AcceptLanguageError::InvalidWeight)?,
            ));
        } else {
            weight = Weight(None);
        }

        Ok(Item { tag, weight })
    }
}

/// A list of BCP47 language tags.
///
/// https://www.rfc-editor.org/info/rfc5664
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTags(Vec<LanguageTag>);

impl LanguageTags {
    pub fn empty() -> Self {
        LanguageTags(Vec::new())
    }

    /// Converts a `Locale` to a set of language tags.
    pub fn from_locale(locale: &Locale) -> Result<LanguageTags, String> {
        let mut tags = Vec::new();

        for locale_range in locale.tags_for("messages") {
            if locale_range == LanguageRange::invariant() {
                continue;
            }

            let str_locale_range = locale_range.as_ref();

            let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| {
                format!(
                    "invalid language tag \"{}\" in locale: {}",
                    str_locale_range, e
                )
            })?;

            if !locale_tag.is_language_range() {
                return Err(format!(
                    "language tag \"{}\" is not a language range",
                    locale_tag
                ));
            }

            tags.push(locale_tag);
        }

        Ok(LanguageTags(tags))
    }

    pub fn from(tags: Vec<LanguageTag>) -> LanguageTags {
        LanguageTags(tags)
    }

    pub fn iter(&self) -> impl Iterator<Item = &LanguageTag> {
        self.0.iter()
    }

    pub fn any_matches(&self, language_tag: &LanguageTag) -> bool {
        self.0.iter().any(|tag| tag.matches(language_tag))
    }
}

impl UserLanguage {
    pub fn new(language: &Language) -> UserLanguage {
        match *language {
            Language::FromEnvironment => UserLanguage::LanguageTags(
                LanguageTags::from_locale(&locale_from_environment())
                    .map_err(|s| {
                        rsvg_log!("could not convert locale to language tags: {}", s);
                    })
                    .unwrap_or_else(|_| LanguageTags::empty()),
            ),

            Language::AcceptLanguage(ref a) => UserLanguage::AcceptLanguage(a.clone()),
        }
    }

    pub fn any_matches(&self, tags: &LanguageTags) -> bool {
        match *self {
            UserLanguage::LanguageTags(ref language_tags) => {
                tags.iter().any(|tag| language_tags.any_matches(tag))
            }
            UserLanguage::AcceptLanguage(ref accept_language) => {
                tags.iter().any(|tag| accept_language.any_matches(tag))
            }
        }
    }
}

/// Gets the user's preferred locale from the environment and
/// translates it to a `Locale` with `LanguageRange` fallbacks.
///
/// The `Locale::current()` call only contemplates a single language,
/// but glib is smarter, and `g_get_langauge_names()` can provide
/// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA
/// English and German).  This function converts the output of
/// `g_get_language_names()` into a `Locale` with appropriate
/// fallbacks.
fn locale_from_environment() -> Locale {
    let mut locale = Locale::invariant();

    for name in glib::get_language_names() {
        if let Ok(range) = LanguageRange::from_unix(&name) {
            locale.add(&range);
        }
    }

    locale
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_accept_language() {
        // plain tag
        assert_eq!(
            AcceptLanguage::parse("es-MX").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(None)
                }]
                .into_boxed_slice()
            )
        );

        // with quality
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // with quality
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=0").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(0.0))
                }]
                .into_boxed_slice()
            )
        );

        // zero decimals are allowed
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=0.").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(0.0))
                }]
                .into_boxed_slice()
            )
        );

        // zero decimals are allowed
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // one decimal
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.0").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // two decimals
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.00").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // three decimals
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.000").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // multiple elements
        assert_eq!(
            AcceptLanguage::parse("es-MX, en; q=0.5").unwrap(),
            AcceptLanguage(
                vec![
                    Item {
                        tag: LanguageTag::parse("es-MX").unwrap(),
                        weight: Weight(None)
                    },
                    Item {
                        tag: LanguageTag::parse("en").unwrap(),
                        weight: Weight(Some(0.5))
                    },
                ]
                .into_boxed_slice()
            )
        );

        // superfluous whitespace
        assert_eq!(
            AcceptLanguage::parse(",es-MX;q=1.000  , en; q=0.125  ,  ,").unwrap(),
            AcceptLanguage(
                vec![
                    Item {
                        tag: LanguageTag::parse("es-MX").unwrap(),
                        weight: Weight(Some(1.0))
                    },
                    Item {
                        tag: LanguageTag::parse("en").unwrap(),
                        weight: Weight(Some(0.125))
                    },
                ]
                .into_boxed_slice()
            )
        );
    }

    #[test]
    fn empty_lists() {
        assert!(matches!(
            AcceptLanguage::parse(""),
            Err(AcceptLanguageError::NoElements)
        ));

        assert!(matches!(
            AcceptLanguage::parse(","),
            Err(AcceptLanguageError::NoElements)
        ));

        assert!(matches!(
            AcceptLanguage::parse(", , ,,,"),
            Err(AcceptLanguageError::NoElements)
        ));
    }

    #[test]
    fn ascii_only() {
        assert!(matches!(
            AcceptLanguage::parse("ës"),
            Err(AcceptLanguageError::InvalidCharacters)
        ));
    }

    #[test]
    fn invalid_tag() {
        assert!(matches!(
            AcceptLanguage::parse("no_underscores"),
            Err(AcceptLanguageError::InvalidLanguageTag(_))
        ));
    }

    #[test]
    fn invalid_weight() {
        assert!(matches!(
            AcceptLanguage::parse("es;"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q="),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=2"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.1"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.12"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.123"),
            Err(AcceptLanguageError::InvalidWeight)
        ));

        // Up to three decimals allowed per RFC 7231
        assert!(matches!(
            AcceptLanguage::parse("es;q=0.1234"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
    }

    #[test]
    fn iter() {
        let accept_language = AcceptLanguage::parse("es-MX, en; q=0.5").unwrap();
        let mut iter = accept_language.iter();

        let (tag, weight) = iter.next().unwrap();
        assert_eq!(*tag, LanguageTag::parse("es-MX").unwrap());
        assert_eq!(weight, 1.0);

        let (tag, weight) = iter.next().unwrap();
        assert_eq!(*tag, LanguageTag::parse("en").unwrap());
        assert_eq!(weight, 0.5);

        assert!(iter.next().is_none());
    }
}
<commit_msg>Clippy - coalesce ifs with same bodies<commit_after>//! Parser for an Accept-Language HTTP header.

use language_tags::{LanguageTag, ParseError};
use locale_config::{LanguageRange, Locale};

use std::error;
use std::fmt;
use std::str::FromStr;

/// Used to set the language for rendering.
///
/// SVG documents can use the `<switch>` element, whose children have a `systemLanguage`
/// attribute; only the first child which has a `systemLanguage` that matches the
/// preferred languages will be rendered.
///
/// This enum, used with `CairoRenderer::with_language`, configures how to obtain the
/// user's prefererred languages.
pub enum Language {
    /// Use the Unix environment variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` to obtain the
    /// user's language.  This uses [`g_get_language_names()`][ggln] underneath.
    ///
    /// [ggln]: https://developer.gnome.org/glib/stable/glib-I18N.html#g-get-language-names
    FromEnvironment,
    AcceptLanguage(AcceptLanguage),
}

/// `Language` but with the environment's locale converted to something we can use.
#[derive(Clone)]
pub enum UserLanguage {
    LanguageTags(LanguageTags),
    AcceptLanguage(AcceptLanguage),
}

#[derive(Clone, Debug, PartialEq)]
struct Weight(Option<f32>);

impl Weight {
    fn numeric(&self) -> f32 {
        self.0.unwrap_or(1.0)
    }
}

#[derive(Clone, Debug, PartialEq)]
struct Item {
    tag: LanguageTag,
    weight: Weight,
}

/// Stores a parsed version of an HTTP Accept-Language header.
///
/// https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5
#[derive(Clone, Debug, PartialEq)]
pub struct AcceptLanguage(Box<[Item]>);

/// Errors when parsing an `AcceptLanguage`.
#[derive(Debug, PartialEq)]
pub enum AcceptLanguageError {
    NoElements,
    InvalidCharacters,
    InvalidLanguageTag(ParseError),
    InvalidWeight,
}

impl error::Error for AcceptLanguageError {}

impl fmt::Display for AcceptLanguageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NoElements => write!(f, "no language tags in list"),
            Self::InvalidCharacters => write!(f, "invalid characters in language list"),
            Self::InvalidLanguageTag(e) => write!(f, "invalid language tag: {}", e),
            Self::InvalidWeight => write!(f, "invalid q= weight"),
        }
    }
}

/// Optional whitespace, Space or Tab, per https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
const OWS: [char; 2] = ['\x20', '\x09'];

impl AcceptLanguage {
    pub fn parse(s: &str) -> Result<AcceptLanguage, AcceptLanguageError> {
        if !s.is_ascii() {
            return Err(AcceptLanguageError::InvalidCharacters);
        }

        let mut items = Vec::new();

        for val in s.split(',') {
            let trimmed = val.trim_matches(&OWS[..]);
            if trimmed.is_empty() {
                continue;
            }

            items.push(Item::parse(trimmed)?);
        }

        if items.is_empty() {
            Err(AcceptLanguageError::NoElements)
        } else {
            Ok(AcceptLanguage(items.into_boxed_slice()))
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = (&LanguageTag, f32)> {
        self.0.iter().map(|item| (&item.tag, item.weight.numeric()))
    }

    fn any_matches(&self, tag: &LanguageTag) -> bool {
        self.iter().any(|(self_tag, _weight)| tag.matches(self_tag))
    }
}

impl Item {
    fn parse(s: &str) -> Result<Item, AcceptLanguageError> {
        let semicolon_pos = s.find(';');

        let (before_semicolon, after_semicolon) = if let Some(semi) = semicolon_pos {
            (&s[..semi], Some(&s[semi + 1..]))
        } else {
            (s, None)
        };

        let tag = LanguageTag::parse(before_semicolon)
            .map_err(AcceptLanguageError::InvalidLanguageTag)?;

        let weight;

        if let Some(quality) = after_semicolon {
            let quality = quality.trim_start_matches(&OWS[..]);

            let number = if let Some(qvalue) = quality.strip_prefix("q=") {
                if qvalue.starts_with(&['0', '1'][..]) {
                    let first_digit = qvalue.chars().next().unwrap();

                    if let Some(decimals) = qvalue[1..].strip_prefix(".") {
                        if (first_digit == '0'
                            && decimals.len() <= 3
                            && decimals.chars().all(|c| c.is_digit(10)))
                            || (first_digit == '1'
                                && decimals.len() <= 3
                                && decimals.chars().all(|c| c == '0'))
                        {
                            qvalue
                        } else {
                            return Err(AcceptLanguageError::InvalidWeight);
                        }
                    } else {
                        qvalue
                    }
                } else {
                    return Err(AcceptLanguageError::InvalidWeight);
                }
            } else {
                return Err(AcceptLanguageError::InvalidWeight);
            };

            weight = Weight(Some(
                f32::from_str(number).map_err(|_| AcceptLanguageError::InvalidWeight)?,
            ));
        } else {
            weight = Weight(None);
        }

        Ok(Item { tag, weight })
    }
}

/// A list of BCP47 language tags.
///
/// https://www.rfc-editor.org/info/rfc5664
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTags(Vec<LanguageTag>);

impl LanguageTags {
    pub fn empty() -> Self {
        LanguageTags(Vec::new())
    }

    /// Converts a `Locale` to a set of language tags.
    pub fn from_locale(locale: &Locale) -> Result<LanguageTags, String> {
        let mut tags = Vec::new();

        for locale_range in locale.tags_for("messages") {
            if locale_range == LanguageRange::invariant() {
                continue;
            }

            let str_locale_range = locale_range.as_ref();

            let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| {
                format!(
                    "invalid language tag \"{}\" in locale: {}",
                    str_locale_range, e
                )
            })?;

            if !locale_tag.is_language_range() {
                return Err(format!(
                    "language tag \"{}\" is not a language range",
                    locale_tag
                ));
            }

            tags.push(locale_tag);
        }

        Ok(LanguageTags(tags))
    }

    pub fn from(tags: Vec<LanguageTag>) -> LanguageTags {
        LanguageTags(tags)
    }

    pub fn iter(&self) -> impl Iterator<Item = &LanguageTag> {
        self.0.iter()
    }

    pub fn any_matches(&self, language_tag: &LanguageTag) -> bool {
        self.0.iter().any(|tag| tag.matches(language_tag))
    }
}

impl UserLanguage {
    pub fn new(language: &Language) -> UserLanguage {
        match *language {
            Language::FromEnvironment => UserLanguage::LanguageTags(
                LanguageTags::from_locale(&locale_from_environment())
                    .map_err(|s| {
                        rsvg_log!("could not convert locale to language tags: {}", s);
                    })
                    .unwrap_or_else(|_| LanguageTags::empty()),
            ),

            Language::AcceptLanguage(ref a) => UserLanguage::AcceptLanguage(a.clone()),
        }
    }

    pub fn any_matches(&self, tags: &LanguageTags) -> bool {
        match *self {
            UserLanguage::LanguageTags(ref language_tags) => {
                tags.iter().any(|tag| language_tags.any_matches(tag))
            }
            UserLanguage::AcceptLanguage(ref accept_language) => {
                tags.iter().any(|tag| accept_language.any_matches(tag))
            }
        }
    }
}

/// Gets the user's preferred locale from the environment and
/// translates it to a `Locale` with `LanguageRange` fallbacks.
///
/// The `Locale::current()` call only contemplates a single language,
/// but glib is smarter, and `g_get_langauge_names()` can provide
/// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA
/// English and German).  This function converts the output of
/// `g_get_language_names()` into a `Locale` with appropriate
/// fallbacks.
fn locale_from_environment() -> Locale {
    let mut locale = Locale::invariant();

    for name in glib::get_language_names() {
        if let Ok(range) = LanguageRange::from_unix(&name) {
            locale.add(&range);
        }
    }

    locale
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_accept_language() {
        // plain tag
        assert_eq!(
            AcceptLanguage::parse("es-MX").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(None)
                }]
                .into_boxed_slice()
            )
        );

        // with quality
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // with quality
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=0").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(0.0))
                }]
                .into_boxed_slice()
            )
        );

        // zero decimals are allowed
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=0.").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(0.0))
                }]
                .into_boxed_slice()
            )
        );

        // zero decimals are allowed
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // one decimal
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.0").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // two decimals
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.00").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // three decimals
        assert_eq!(
            AcceptLanguage::parse("es-MX;q=1.000").unwrap(),
            AcceptLanguage(
                vec![Item {
                    tag: LanguageTag::parse("es-MX").unwrap(),
                    weight: Weight(Some(1.0))
                }]
                .into_boxed_slice()
            )
        );

        // multiple elements
        assert_eq!(
            AcceptLanguage::parse("es-MX, en; q=0.5").unwrap(),
            AcceptLanguage(
                vec![
                    Item {
                        tag: LanguageTag::parse("es-MX").unwrap(),
                        weight: Weight(None)
                    },
                    Item {
                        tag: LanguageTag::parse("en").unwrap(),
                        weight: Weight(Some(0.5))
                    },
                ]
                .into_boxed_slice()
            )
        );

        // superfluous whitespace
        assert_eq!(
            AcceptLanguage::parse(",es-MX;q=1.000  , en; q=0.125  ,  ,").unwrap(),
            AcceptLanguage(
                vec![
                    Item {
                        tag: LanguageTag::parse("es-MX").unwrap(),
                        weight: Weight(Some(1.0))
                    },
                    Item {
                        tag: LanguageTag::parse("en").unwrap(),
                        weight: Weight(Some(0.125))
                    },
                ]
                .into_boxed_slice()
            )
        );
    }

    #[test]
    fn empty_lists() {
        assert!(matches!(
            AcceptLanguage::parse(""),
            Err(AcceptLanguageError::NoElements)
        ));

        assert!(matches!(
            AcceptLanguage::parse(","),
            Err(AcceptLanguageError::NoElements)
        ));

        assert!(matches!(
            AcceptLanguage::parse(", , ,,,"),
            Err(AcceptLanguageError::NoElements)
        ));
    }

    #[test]
    fn ascii_only() {
        assert!(matches!(
            AcceptLanguage::parse("ës"),
            Err(AcceptLanguageError::InvalidCharacters)
        ));
    }

    #[test]
    fn invalid_tag() {
        assert!(matches!(
            AcceptLanguage::parse("no_underscores"),
            Err(AcceptLanguageError::InvalidLanguageTag(_))
        ));
    }

    #[test]
    fn invalid_weight() {
        assert!(matches!(
            AcceptLanguage::parse("es;"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q="),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=2"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.1"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.12"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
        assert!(matches!(
            AcceptLanguage::parse("es;q=1.123"),
            Err(AcceptLanguageError::InvalidWeight)
        ));

        // Up to three decimals allowed per RFC 7231
        assert!(matches!(
            AcceptLanguage::parse("es;q=0.1234"),
            Err(AcceptLanguageError::InvalidWeight)
        ));
    }

    #[test]
    fn iter() {
        let accept_language = AcceptLanguage::parse("es-MX, en; q=0.5").unwrap();
        let mut iter = accept_language.iter();

        let (tag, weight) = iter.next().unwrap();
        assert_eq!(*tag, LanguageTag::parse("es-MX").unwrap());
        assert_eq!(weight, 1.0);

        let (tag, weight) = iter.next().unwrap();
        assert_eq!(*tag, LanguageTag::parse("en").unwrap());
        assert_eq!(weight, 0.5);

        assert!(iter.next().is_none());
    }
}
<|endoftext|>


<commit_before>// This module defines constants and functions for working with zakocmp
// configuration files.

use std::io::{Error, ErrorKind};
use std::result::Result;

// Enumerates the string representations of known policies.
const POLICY_REPR_IGNORE: &str = "ignore";
const POLICY_REPR_NOADD: &str = "noadd";
const POLICY_REPR_NODELETE: &str = "nodelete";
const POLICY_REPR_NOMODIFY: &str = "nomodify";
const POLICY_REPR_IMMUTABLE: &str = "immutable";

// Represents known policies as integers.
pub const POLICY_IGNORE: i32 = 0;
pub const POLICY_NOADD: i32 = 1 << 0;
pub const POLICY_NODELETE: i32 = 1 << 1;
pub const POLICY_NOMODIFY: i32 = 1 << 2;
pub const POLICY_IMMUTABLE: i32 = POLICY_NOADD | POLICY_NODELETE | POLICY_NOMODIFY;

// Borrows the string representation of one policy `token` and returns
// the equivalent integral representation.
fn policy_token_as_int(token: &str) -> Result<i32, Error> {
    match token {
        POLICY_REPR_IGNORE => Ok(POLICY_IGNORE),
        POLICY_REPR_NOADD => Ok(POLICY_NOADD),
        POLICY_REPR_NODELETE => Ok(POLICY_NODELETE),
        POLICY_REPR_NOMODIFY => Ok(POLICY_NOMODIFY),
        POLICY_REPR_IMMUTABLE => Ok(POLICY_IMMUTABLE),
        _ => Err(Error::new(
            ErrorKind::InvalidInput,
            format!("bad token: ``{}''", token),
        )),
    }
}

/// Borrows the string representation of a combined `policy` and returns
/// the equivalent integral representation. This function expects
/// `policy` to comprise one or more policy tokens separated by commas.
///
/// # Examples
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(&"immutable").unwrap();
/// assert!(policy == config::POLICY_IMMUTABLE);
/// ```
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(&"noadd").unwrap();
/// assert!(policy == config::POLICY_NOADD);
/// ```
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(&"nomodify").unwrap();
/// assert!(policy == config::POLICY_NOMODIFY);
/// ```
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(&"nodelete").unwrap();
/// assert!(policy == config::POLICY_NODELETE);
/// ```
///
/// ```
/// # mod zakocmp;
/// // While it's pointless to repeat oneself,
/// // it's not invalid to do so.
/// let policy: i32 = config::policy_repr_as_int(
///     &"immutable,immutable,immutable").unwrap();
/// assert!(policy == config::POLICY_IMMUTABLE);
/// ```
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(
///     &"noadd,nodelete,nomodify").unwrap();
/// assert!(policy == config::POLICY_IMMUTABLE);
/// ```
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(
///     &"noadd,nomodify").unwrap();
/// assert!(policy == config::POLICY_NOADD | config::POLICY_NOMODIFY);
/// ```
///
/// # Failures
///
/// ```
/// # mod zakocmp;
/// // Obi-wan was witty, but not good at dictating zakocmp policy.
/// assert!(!config::policy_repr_as_int(&"hello there!").is_ok());
/// ```
///
pub fn policy_repr_as_int(policy: &str) -> Result<i32, Error> {
    let mut policy_int: i32 = POLICY_IGNORE;
    for token in policy.split(",") {
        policy_int |= policy_token_as_int(token)?;
    }
    return Ok(policy_int);
}
<commit_msg>zakocmp: rework config unit tests<commit_after>// This module defines constants and functions for working with zakocmp
// configuration files.

use std::io::{Error, ErrorKind};
use std::result::Result;

// Enumerates the string representations of known policies.
const POLICY_REPR_IGNORE: &str = "ignore";
const POLICY_REPR_NOADD: &str = "noadd";
const POLICY_REPR_NODELETE: &str = "nodelete";
const POLICY_REPR_NOMODIFY: &str = "nomodify";
const POLICY_REPR_IMMUTABLE: &str = "immutable";

// Represents known policies as integers.
pub const POLICY_IGNORE: i32 = 0;
pub const POLICY_NOADD: i32 = 1 << 0;
pub const POLICY_NODELETE: i32 = 1 << 1;
pub const POLICY_NOMODIFY: i32 = 1 << 2;
pub const POLICY_IMMUTABLE: i32 = POLICY_NOADD | POLICY_NODELETE | POLICY_NOMODIFY;

// Borrows the string representation of one policy `token` and returns
// the equivalent integral representation.
fn policy_token_as_int(token: &str) -> Result<i32, Error> {
    match token {
        POLICY_REPR_IGNORE => Ok(POLICY_IGNORE),
        POLICY_REPR_NOADD => Ok(POLICY_NOADD),
        POLICY_REPR_NODELETE => Ok(POLICY_NODELETE),
        POLICY_REPR_NOMODIFY => Ok(POLICY_NOMODIFY),
        POLICY_REPR_IMMUTABLE => Ok(POLICY_IMMUTABLE),
        _ => Err(Error::new(
            ErrorKind::InvalidInput,
            format!("bad token: ``{}''", token),
        )),
    }
}

/// Borrows the string representation of a combined `policy` and returns
/// the equivalent integral representation. This function expects
/// `policy` to comprise one or more policy tokens separated by commas.
///
/// # Examples
///
/// ```
/// # mod zakocmp;
/// let policy: i32 = config::policy_repr_as_int(&"immutable").unwrap();
/// assert!(policy == config::POLICY_IMMUTABLE);
/// ```
///
/// ```
/// # mod zakocmp;
/// // Multiple policies are bitwise OR'd together.
/// let policy: i32 = config::policy_repr_as_int(
///     &"noadd,nomodify").unwrap();
/// assert!(policy == config::POLICY_NOADD | config::POLICY_NOMODIFY);
/// ```
///
/// ```
/// # mod zakocmp;
/// // The biggest bitwise OR equals POLICY_IMMUTABLE.
/// let policy: i32 = config::policy_repr_as_int(
///     &"noadd,nodelete,nomodify").unwrap();
/// assert!(policy == config::POLICY_IMMUTABLE);
/// ```
///
/// # Failures
///
/// ```
/// # mod zakocmp;
/// // Obi-wan was witty, but not good at dictating zakocmp policy.
/// assert!(!config::policy_repr_as_int(&"hello there!").is_ok());
/// ```
///
pub fn policy_repr_as_int(policy: &str) -> Result<i32, Error> {
    let mut policy_int: i32 = POLICY_IGNORE;
    for token in policy.split(",") {
        policy_int |= policy_token_as_int(token)?;
    }
    return Ok(policy_int);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bare_noadd() {
        let policy: i32 = match policy_repr_as_int(&"noadd") {
            Ok(value) => value,
            Err(oof) => panic!(oof),
        };
        assert!(policy == POLICY_NOADD);
    }

    #[test]
    fn test_bare_nodelete() {
        let policy: i32 = match policy_repr_as_int(&"nodelete") {
            Ok(value) => value,
            Err(oof) => panic!(oof),
        };
        assert!(policy == POLICY_NODELETE);
    }

    #[test]
    fn test_bare_nomodify() {
        let policy: i32 = match policy_repr_as_int(&"nomodify") {
            Ok(value) => value,
            Err(oof) => panic!(oof),
        };
        assert!(policy == POLICY_NOMODIFY);
    }

    #[test]
    fn test_combo_policy() {
        let policy: i32 = match policy_repr_as_int(&"noadd,nodelete") {
            Ok(value) => value,
            Err(oof) => panic!(oof),
        };
        assert!(policy == POLICY_NOADD | POLICY_NODELETE);
    }

    #[test]
    fn test_repetitive_policy() {
        let policy: i32 = match policy_repr_as_int(
                &"noadd,noadd,noadd,noadd,nodelete,nodelete,nodelete,noadd") {
            Ok(value) => value,
            Err(oof) => panic!(oof),
        };
        assert!(policy == POLICY_NOADD | POLICY_NODELETE);
    }
}
<|endoftext|>


<commit_before>use actions::Action;
use notify::Event;
use std::io::{self, Write};
use std::process::{Command, Stdio};

pub struct CommandAction {
    command_line: String
}

impl CommandAction {
    pub fn new(command_line: String) -> CommandAction {
        CommandAction {
            command_line: command_line
        }
    }

    pub fn get_command_line(&self, event: &Event) -> String {
        let path = match event.path {
            None => "",
            Some(ref path) => path.to_str().unwrap_or("")
        };

        self.command_line.replace("{:p}", path)
    }

    pub fn get_command(&self, event: &Event) -> Command {
        let command_line = self.get_command_line(event);
        let mut cmd_pieces = command_line.split(" ");
        let mut command = Command::new(cmd_pieces.next().unwrap());
        for piece in cmd_pieces {
            command.arg(piece);
        }

        command
    }
}

impl Action for CommandAction {
    fn handle_change(&self, event: &Event) -> Result<(), ()> {
        let mut command = self.get_command(event);
        let command_result = command
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .output();

        match command_result {
            Err(_) => {
                println!("Could not execute command: {:?}", self.command_line);
                Err(())
            },
            Ok(output) => {
                println!("{}", String::from_utf8_lossy(&output.stdout));

                if !output.stderr.is_empty() {
                    let write_result = writeln!(
                        &mut io::stderr(),
                        "{}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                    match write_result {
                        Err(x) => println!("Error: Unable to write to stderr: {}", x),
                        Ok(_) => {}
                    }
                }

                Ok(())
            }
        }
    }
}
<commit_msg>Add basic CommandAction tests<commit_after>use actions::Action;
use notify::Event;
use std::io::{self, Write};
use std::process::{Command, Stdio};

pub struct CommandAction {
    command_line: String
}

impl CommandAction {
    pub fn new(command_line: String) -> CommandAction {
        CommandAction {
            command_line: command_line
        }
    }

    pub fn get_command_line(&self, event: &Event) -> String {
        let path = match event.path {
            None => "",
            Some(ref path) => path.to_str().unwrap_or("")
        };

        self.command_line.replace("{:p}", path)
    }

    pub fn get_command(&self, event: &Event) -> Command {
        let command_line = self.get_command_line(event);
        let mut cmd_pieces = command_line.split(" ");
        let mut command = Command::new(cmd_pieces.next().unwrap());
        for piece in cmd_pieces {
            command.arg(piece);
        }

        command
    }
}

impl Action for CommandAction {
    fn handle_change(&self, event: &Event) -> Result<(), ()> {
        let mut command = self.get_command(event);
        let command_result = command
            .stdin(Stdio::inherit())
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .output();

        match command_result {
            Err(_) => {
                println!("Could not execute command: {:?}", self.command_line);
                Err(())
            },
            Ok(output) => {
                println!("{}", String::from_utf8_lossy(&output.stdout));

                if !output.stderr.is_empty() {
                    let write_result = writeln!(
                        &mut io::stderr(),
                        "{}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                    match write_result {
                        Err(x) => println!("Error: Unable to write to stderr: {}", x),
                        Ok(_) => {}
                    }
                }

                Ok(())
            }
        }
    }
}

#[cfg(test)]
mod test {
    extern crate rand;

    use super::*;

    use actions::Action;
    use notify::{Event, Op};
    use std::path::PathBuf;

    #[test]
    fn constructor() {
        // Make sure new() works with a single string argument
        let _ = CommandAction::new("date".to_string());
    }

    #[test]
    fn handle_change_existing_command() {
        let o = Op::empty();
        let path_buf = PathBuf::from("/");

        let event = Event {
            path: Some(path_buf),
            op: Ok(o)
        };

        // Assume the "date" command exists on all platforms
        let command = CommandAction::new("date".to_string());
        let result = command.handle_change(&event);

        // We can't capture the output, so just make sure the function
        // returns Ok.
        assert!(result.is_ok());
    }

    #[test]
    fn handle_change_missing_command() {
        let o = Op::empty();
        let path_buf = PathBuf::from("/");

        let event = Event {
            path: Some(path_buf),
            op: Ok(o)
        };

        // Assume this command does not exist
        let command = CommandAction::new("command_does_not_exist".to_string());
        let result = command.handle_change(&event);

        assert!(result.is_err());
    }

}
<|endoftext|>