portable_link_section 0.1.0

A portable version of the #[link_section] macro.
Documentation
use crate::error::{Error, InvalidGroupParameterReason};
use proc_macro2::{
    Delimiter as Delimiter2, Ident as Ident2, Span as Span2, TokenStream as TokenStream2,
    TokenTree as TokenTree2, token_stream::IntoIter as TokenTreeIter,
};
use quote::ToTokens;

pub enum LinkSegment {
    Text(TextSegmentGroup),
    Data(DataSegmentGroup),
    Rodata,
    Bss,
}

impl TryFrom<TokenTreeIter> for LinkSegment {
    type Error = Error;

    fn try_from(tokens: TokenTreeIter) -> Result<Self, Self::Error> {
        let mut tokens = tokens;
        let segment = match tokens.next() {
            Some(TokenTree2::Ident(segment)) => segment,
            Some(invalid) => return Err(Error::SegmentParameterTypeMismatch(invalid)),
            None => return Err(Error::EmptyParams),
        };
        match segment {
            segment if segment == "text" => {
                let group = Self::parse_segment_group(segment.span(), tokens)?;
                let text_group = TextSegmentGroup::try_from(group)?;
                Ok(Self::Text(text_group))
            }
            segment if segment == "data" => {
                let group = Self::parse_segment_group(segment.span(), tokens)?;
                let data_group = DataSegmentGroup::try_from(group)?;
                Ok(Self::Data(data_group))
            }
            segment if segment == "rodata" => {
                if let Some(invalid) = tokens.next() {
                    return Err(Error::ExtraneousParameter(invalid));
                }
                Ok(Self::Rodata)
            }
            segment if segment == "bss" => {
                if let Some(invalid) = tokens.next() {
                    return Err(Error::ExtraneousParameter(invalid));
                }
                Ok(Self::Bss)
            }
            invalid => Err(Error::InvalidSegmentValue(invalid)),
        }
    }
}

impl LinkSegment {
    fn parse_segment_group(
        segment_span: Span2,
        tokens: TokenTreeIter,
    ) -> Result<SegmentGroup, Error> {
        let mut tokens = tokens;
        let Some(group) = tokens.next() else {
            return Err(Error::MissingGroupParameter(segment_span));
        };
        if let Some(extraneous) = tokens.next() {
            return Err(Error::ExtraneousParameter(extraneous));
        }
        let group_span = group.span();
        let TokenTree2::Group(group) = group else {
            return Err(Error::GroupParameterTypeMismatch(group_span));
        };
        if group.delimiter() != Delimiter2::Parenthesis {
            return Err(Error::InvalidGroupDelimiter(group_span));
        }
        let mut group_tts = group.stream().into_iter();
        let Some(TokenTree2::Ident(group)) = group_tts.next() else {
            return Err(Error::GroupParameterTypeMismatch(group_span));
        };
        if let Some(extraneous) = group_tts.next() {
            return Err(Error::ExtraneousParameter(extraneous));
        }
        SegmentGroup::try_from(group)
    }
}

struct SegmentGroup(Ident2);

impl ToTokens for SegmentGroup {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        self.0.to_tokens(tokens)
    }
}

impl TryFrom<Ident2> for SegmentGroup {
    type Error = Error;

    fn try_from(value: Ident2) -> Result<Self, Self::Error> {
        let name = value.to_string();
        if name.len() > 15 {
            // Note:
            // - MACH-O maximum is 16.
            // - MACH-O strings are zero-terminated therefore practical maximum is 15.
            return Err(Error::InvalidGroupValue(
                value,
                InvalidGroupParameterReason::TooLong(name.len()),
            ));
        }
        let invalid_symbol = name.find(|ch| match ch {
            // Note: we disallow upper-case since lower-case is more accepted.
            'a'..='z' | '0'..='9' | '_' => false,
            _ => true,
        });
        if let Some(invalid_symbol) = invalid_symbol {
            let ch = name.as_bytes()[invalid_symbol] as char;
            return Err(Error::InvalidGroupValue(
                value,
                InvalidGroupParameterReason::InvalidSymbol(ch),
            ));
        }
        Ok(Self(value))
    }
}

pub struct TextSegmentGroup(SegmentGroup);

impl ToTokens for TextSegmentGroup {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        self.0.to_tokens(tokens)
    }
}

impl TryFrom<SegmentGroup> for TextSegmentGroup {
    type Error = Error;

    fn try_from(group: SegmentGroup) -> Result<Self, Self::Error> {
        let span = group.0.span();
        let name = group.0.to_string();
        match name.as_str() {
            "const" | "cstring" | "gcc_except_tab" | "eh_frame" => {
                return Err(Error::ReservedGroupParameter(span));
            }
            _ => {}
        };
        Ok(Self(group))
    }
}

pub struct DataSegmentGroup(SegmentGroup);

impl ToTokens for DataSegmentGroup {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        self.0.to_tokens(tokens)
    }
}

impl TryFrom<SegmentGroup> for DataSegmentGroup {
    type Error = Error;

    #[rustfmt::skip]
    fn try_from(group: SegmentGroup) -> Result<Self, Self::Error> {
        let span = group.0.span();
        let name = group.0.to_string();
        match name.as_str() {
            | "la_symbol_ptr"
            | "nl_symbol_ptr"
            | "mod_init_func"
            | "mod_term_func"
            | "common"
            | "bss" => {
                return Err(Error::ReservedGroupParameter(span));
            }
            _ => {}
        }
        Ok(Self(group))
    }
}