use crate::{
detection::find_bytes,
formats::BinaryContext,
metadata::{BuildInfo, DepEntry, DepReplacement},
structures::util::read_uvarint,
};
const BUILDINFO_MAGIC: &[u8] = b"\xff Go buildinf:";
const BUILDINFO_ALIGN: usize = 16;
const BUILDINFO_HEADER_SIZE: usize = 32;
const MOD_INFO_START: &[u8] = &[
0x30, 0x77, 0xaf, 0x0c, 0x92, 0x74, 0x08, 0x02, 0x41, 0xe1, 0xc1, 0x07, 0xe6, 0xd6, 0x18, 0xe6,
];
const MOD_INFO_END: &[u8] = &[
0xf9, 0x32, 0x43, 0x31, 0x86, 0x18, 0x20, 0x72, 0x00, 0x82, 0x42, 0x10, 0x41, 0x16, 0xd8, 0xf2,
];
const FLAG_ENDIAN: u8 = 0x01;
const FLAG_VERSION_INL: u8 = 0x02;
pub fn extract<'a>(ctx: &BinaryContext<'a>) -> Option<BuildInfo<'a>> {
let data = ctx.data();
let header_start = find_magic(ctx, data)?;
let header_end = header_start.checked_add(BUILDINFO_HEADER_SIZE)?;
let header = data.get(header_start..header_end)?;
let ptr_size = (*header.get(14)?) as usize;
let flags = *header.get(15)?;
let _is_big_endian = (flags & FLAG_ENDIAN) != 0;
let is_inline = (flags & FLAG_VERSION_INL) != 0;
if ptr_size != 4 && ptr_size != 8 {
return None;
}
let mut info = BuildInfo::default();
if is_inline {
let payload = data.get(header_end..)?;
let (version, rest) = read_varint_string(payload)?;
info.go_version = Some(version);
let (modinfo_bytes, _) = read_varint_bytes(rest)?;
if let Some(text) = extract_modinfo_text(modinfo_bytes) {
parse_modinfo(text, &mut info);
}
} else {
info.go_version = find_version_string(data);
}
Some(info)
}
fn find_magic(ctx: &BinaryContext<'_>, data: &[u8]) -> Option<usize> {
let sections = ctx.sections();
let candidates = [
sections.go_buildinfo.as_ref(),
sections.data_section.as_ref(),
sections.noptrdata.as_ref(),
];
for range in candidates.into_iter().flatten() {
let end = range.offset.checked_add(range.size)?.min(data.len());
if let Some(region) = data.get(range.offset..end)
&& let Some(pos) = find_aligned_magic(region)
{
return range.offset.checked_add(pos);
}
}
for (from, to) in ctx.data_regions() {
if let Some(region) = data.get(from..to)
&& let Some(pos) = find_aligned_magic(region)
{
return from.checked_add(pos);
}
}
None
}
fn find_aligned_magic(data: &[u8]) -> Option<usize> {
let mut first_unaligned: Option<usize> = None;
let mut from: usize = 0;
while let Some(rel) = data
.get(from..)
.and_then(|d| find_bytes(d, BUILDINFO_MAGIC))
{
let at = from.checked_add(rel)?;
if at
.checked_add(BUILDINFO_HEADER_SIZE)
.is_none_or(|e| e > data.len())
{
break;
}
if at.checked_rem(BUILDINFO_ALIGN) == Some(0) {
return Some(at);
}
first_unaligned.get_or_insert(at);
from = at.checked_add(1)?;
}
first_unaligned
}
fn read_varint_string(data: &[u8]) -> Option<(&str, &[u8])> {
let (len, consumed) = read_uvarint(data)?;
let len = len as usize;
let end = consumed.checked_add(len)?;
let bytes = data.get(consumed..end)?;
let s = std::str::from_utf8(bytes).ok()?;
let rest = data.get(end..)?;
Some((s, rest))
}
fn read_varint_bytes(data: &[u8]) -> Option<(&[u8], &[u8])> {
let (len, consumed) = read_uvarint(data)?;
let len = len as usize;
let end = consumed.checked_add(len)?;
let payload = data.get(consumed..end)?;
let rest = data.get(end..)?;
Some((payload, rest))
}
fn extract_modinfo_text(data: &[u8]) -> Option<&str> {
let start = if data.len() >= 16 && data.get(..16) == Some(MOD_INFO_START) {
16
} else {
0
};
let end = if data.len() >= 16 {
let tail_start = data.len().checked_sub(16)?;
if data.get(tail_start..) == Some(MOD_INFO_END) {
tail_start
} else {
data.len()
}
} else {
data.len()
};
if start >= end {
return None;
}
std::str::from_utf8(data.get(start..end)?).ok()
}
fn parse_modinfo<'a>(text: &'a str, info: &mut BuildInfo<'a>) {
for line in text.lines() {
let parts: Vec<&'a str> = line.splitn(4, '\t').collect();
match parts.first() {
Some(&"path") => {
if let Some(p) = parts.get(1) {
info.main_path = Some(*p);
}
}
Some(&"mod") => {
if let Some(m) = parts.get(1) {
info.main_module = Some(*m);
}
if let Some(v) = parts.get(2) {
info.main_version = Some(*v);
}
if let Some(s) = parts.get(3) {
info.main_module_sum = Some(*s);
}
}
Some(&"dep") => {
if let Some(p) = parts.get(1) {
info.deps.push(DepEntry {
path: p,
version: parts.get(2).copied(),
sum: parts.get(3).copied(),
replacement: None,
});
}
}
Some(&"=>") => {
if let Some(p) = parts.get(1) {
let replacement = DepReplacement {
path: p,
version: parts.get(2).copied(),
sum: parts.get(3).copied(),
};
if let Some(last) = info.deps.last_mut() {
last.replacement = Some(replacement);
}
}
}
Some(&"build") => {
let setting = match parts.get(1) {
Some(s) => *s,
None => continue,
};
if let Some((key, value)) = setting.split_once('=') {
info.build_settings.push((key, value));
} else {
info.build_settings.push((setting, ""));
}
}
_ => {}
}
}
}
pub fn find_version_string(data: &[u8]) -> Option<&str> {
let pattern = b"go1.";
let mut pos: usize = 0;
loop {
let cutoff = pos.checked_add(8)?;
if cutoff >= data.len() {
return None;
}
let window = data.get(pos..)?;
let found = find_bytes(window, pattern)?;
let start = pos.checked_add(found)?;
let scan_start = start.checked_add(4)?;
let scan_limit = start.checked_add(20)?.min(data.len());
let mut end = scan_start;
while end < scan_limit {
let ch = match data.get(end) {
Some(c) => *c,
None => break,
};
if ch.is_ascii_digit() || ch == b'.' {
end = end.checked_add(1)?;
} else {
break;
}
}
if let Some(slice) = data.get(start..end)
&& let Ok(s) = std::str::from_utf8(slice)
&& s.len() >= 5
&& s.get(4..)
.is_some_and(|tail| tail.starts_with(|c: char| c.is_ascii_digit()))
{
return Some(s);
}
pos = scan_start;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_varint_string() {
let data = [0x05, b'h', b'e', b'l', b'l', b'o', 0x00];
let (s, rest) = read_varint_string(&data).unwrap();
assert_eq!(s, "hello");
assert_eq!(rest.len(), 1);
}
#[test]
fn test_find_version_string() {
let mut data = vec![0u8; 100];
data[50..58].copy_from_slice(b"go1.26.1");
data[58] = 0;
assert_eq!(find_version_string(&data), Some("go1.26.1"));
}
#[test]
fn test_parse_modinfo() {
let text = "path\texample.com/app\nmod\texample.com/app\t(devel)\ndep\texample.com/dep\tv1.0.0\nbuild\t-compiler=gc\nbuild\tGOOS=linux\n";
let mut info = BuildInfo::default();
parse_modinfo(text, &mut info);
assert_eq!(info.main_path, Some("example.com/app"));
assert_eq!(info.main_module, Some("example.com/app"));
assert_eq!(info.deps.len(), 1);
assert_eq!(info.deps[0].path, "example.com/dep");
assert_eq!(info.deps[0].version, Some("v1.0.0"));
assert_eq!(info.deps[0].sum, None);
assert!(info.deps[0].replacement.is_none());
assert_eq!(info.build_settings.len(), 2);
}
#[test]
fn test_parse_modinfo_with_sum_and_replace() {
let text = "\
path\texample.com/app
dep\texample.com/lib\tv1.0.0\th1:abc=
=>\texample.com/forked\tv1.0.1\th1:def=
dep\texample.com/local\tv0.0.0
=>\t./vendored
";
let mut info = BuildInfo::default();
parse_modinfo(text, &mut info);
assert_eq!(info.deps.len(), 2);
assert_eq!(info.deps[0].sum, Some("h1:abc="));
let r0 = info.deps[0].replacement.as_ref().unwrap();
assert_eq!(r0.path, "example.com/forked");
assert_eq!(r0.version, Some("v1.0.1"));
assert_eq!(r0.sum, Some("h1:def="));
let r1 = info.deps[1].replacement.as_ref().unwrap();
assert_eq!(r1.path, "./vendored");
assert_eq!(r1.version, None);
}
}