editres 0.1.6

Embed data into executables after build
Documentation
/*
 * 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 https://mozilla.org/MPL/2.0/.
 */
use crate::names::SENTINEL_SUFFIX;
use anyhow::Context;
use std::collections::btree_map::Entry;
use std::mem::size_of;
use std::num::NonZeroU16;
use std::str::from_utf8;

use std::collections::BTreeMap;

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ResourceEntry {
    Exists { id: NonZeroU16 },
    None { id_positions: Vec<usize> },
}

pub(crate) fn list_for_injection<'a>(
    executable_buffer: &'a [u8],
) -> anyhow::Result<BTreeMap<&'a str, ResourceEntry>> {
    let mut resources = BTreeMap::<&'a str, ResourceEntry>::new();
    let sentiel_indices = executable_buffer
        .windows(SENTINEL_SUFFIX.len())
        .enumerate()
        .filter_map(|(idx, window)| {
            if window == SENTINEL_SUFFIX.as_bytes() {
                Some(idx)
            } else {
                None
            }
        });
    for sentiel_pos in sentiel_indices {
        let name_split_pos = executable_buffer[..sentiel_pos]
            .iter()
            .rposition(|byte| *byte == b'|')
            .context("Not name len found before the sentinel")?;
        let name_len_str = from_utf8(&executable_buffer[name_split_pos + 1..sentiel_pos])
            .context("name len is not utf8")?;
        let name_len: usize = name_len_str.parse().context("name len is not a number")?;
        let name_pos = name_split_pos - name_len;
        let name =
            from_utf8(&executable_buffer[name_pos..name_split_pos]).context("name is not utf8")?;
        let id_pos = name_pos - size_of::<u16>();
        let id_bytes: [u8; size_of::<u16>()] =
            executable_buffer[id_pos..name_pos].try_into().unwrap();
        let id = u16::from_be_bytes(id_bytes);
        match (resources.entry(name), NonZeroU16::new(id)) {
            (Entry::Vacant(vacant), id) => {
                vacant.insert(if let Some(id) = id {
                    ResourceEntry::Exists { id }
                } else {
                    ResourceEntry::None {
                        id_positions: vec![id_pos],
                    }
                });
            }
            (Entry::Occupied(mut occupied), id) => match (occupied.get_mut(), id) {
                (ResourceEntry::Exists { id: existing_id }, id) => {
                    if id != Some(*existing_id) {
                        anyhow::bail!("ids aren't the same ({existing_id} != {id:?}) between different sentiels of resource {name}");
                    }
                }
                (ResourceEntry::None { .. }, Some(id)) => {
                    anyhow::bail!("ids aren't the same (None != {id}) between different sentiels of resource {name}");
                }
                (ResourceEntry::None { id_positions }, None) => {
                    id_positions.push(id_pos);
                }
            },
        }
    }
    Ok(resources)
}

/// Lists declared resources and whether they have been injected.
/// # Example
/// ```rust
/// # use editres::injector::list;
/// # let _ = || -> anyhow::Result<()> {
/// let executable_buffer = std::fs::read("a.out")?;
/// let resources = list(&executable_buffer)?;
/// assert_eq!(resources, &[("my_res".to_string(), false)]);
/// # Ok(())
/// # };
/// ```
pub fn list(executable_buffer: &[u8]) -> anyhow::Result<Vec<(String, bool)>> {
    let resources = list_for_injection(&executable_buffer)?;

    Ok(resources
        .into_iter()
        .map(|(name, entry)| {
            (
                name.to_string(),
                matches!(entry, ResourceEntry::Exists { .. }),
            )
        })
        .collect())
}