use std::fs;
use std::path::Path;
use anyhow::{bail, Context, Result};
pub const CHANNEL_TAG_OID: &str = "1.3.6.1.4.1.11129.2.1.9999";
pub static OID_SEARCH_BYTES: [u8; 15] = [0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x01, 0xce, 0x0f, 0x04, 0x82];
pub static CHANNEL_TAG_MAGIC: [u8; 32] = [
0x73, 0x12, 0x9e, 0x58, 0x64, 0xb5, 0x7b, 0x41, 0xfb, 0xca, 0xdb, 0x9d, 0x0b, 0xd5, 0x3f, 0x9d, 0x70, 0xb0, 0x23, 0x71, 0xe8, 0xc7, 0xfd, 0x6b, 0x7f, 0xfe, 0x30, 0x5f, 0x14, 0x47, 0x9e, 0x2f, ];
pub const FORMAT_VERSION: u8 = 0x01;
pub const MAX_CHANNEL_LEN: usize = 64;
const RECORD_HEADER_LEN: usize = 35;
fn is_valid_channel_byte(b: u8) -> bool {
b == b'-' || b.is_ascii_digit() || b.is_ascii_lowercase()
}
fn is_valid_channel(channel: &str) -> bool {
!channel.is_empty() && channel.len() <= MAX_CHANNEL_LEN && channel.bytes().all(is_valid_channel_byte)
}
pub fn read_channel_from_signature(raw_sig: &[u8]) -> Option<String> {
let mut last_valid: Option<String> = None;
let mut pos = 0usize;
while pos < raw_sig.len() {
let Some(rel) = raw_sig[pos..].windows(OID_SEARCH_BYTES.len()).position(|w| w == OID_SEARCH_BYTES) else {
break;
};
let marker = pos + rel;
pos = marker + 1;
let len_off = marker + OID_SEARCH_BYTES.len();
let Some(n_bytes) = raw_sig.get(len_off..len_off + 2) else { continue };
let n = u16::from_be_bytes([n_bytes[0], n_bytes[1]]) as usize;
if n < RECORD_HEADER_LEN + 1 {
continue;
}
let rec_off = len_off + 2;
let Some(record) = raw_sig.get(rec_off..rec_off + n) else { continue };
if record[0..32] != CHANNEL_TAG_MAGIC {
continue;
}
if record[32] != FORMAT_VERSION {
continue;
}
let l = u16::from_le_bytes([record[33], record[34]]) as usize;
if !(1..=MAX_CHANNEL_LEN).contains(&l) || RECORD_HEADER_LEN + l > n {
continue;
}
let channel_bytes = &record[RECORD_HEADER_LEN..RECORD_HEADER_LEN + l];
if !channel_bytes.iter().copied().all(is_valid_channel_byte) {
continue;
}
if let Ok(channel) = std::str::from_utf8(channel_bytes) {
last_valid = Some(channel.to_string());
}
}
last_valid
}
pub fn patch_installed_channel(root_app_dir: &Path, channel: &str) -> Result<()> {
if !is_valid_channel(channel) {
bail!("Refusing to patch invalid channel (must match ^[a-z0-9-]{{1,64}}$)");
}
let manifest_path = root_app_dir.join("current").join("sq.version");
let xml = fs::read_to_string(&manifest_path).with_context(|| format!("Failed to read manifest at {:?}", manifest_path))?;
const START_TAG: &str = "<channel>";
const END_TAG: &str = "</channel>";
let start = xml.find(START_TAG).with_context(|| format!("No <channel> element found in {:?}", manifest_path))?;
let inner_start = start + START_TAG.len();
let inner_len = xml[inner_start..].find(END_TAG).with_context(|| format!("Unterminated <channel> element in {:?}", manifest_path))?;
let mut patched = String::with_capacity(xml.len() + channel.len());
patched.push_str(&xml[..inner_start]);
patched.push_str(channel);
patched.push_str(&xml[inner_start + inner_len..]);
if patched != xml {
let tmp_path = manifest_path.with_extension("version.tmp");
fs::write(&tmp_path, &patched).with_context(|| format!("Failed to write patched manifest at {:?}", tmp_path))?;
fs::rename(&tmp_path, &manifest_path).with_context(|| format!("Failed to replace manifest at {:?}", manifest_path))?;
}
Ok(())
}
#[doc(hidden)]
pub mod test_support {
use std::fs;
use std::path::{Path, PathBuf};
pub const GOLDEN_VECTOR_BETA: [u8; 56] = [
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x01, 0xce, 0x0f, 0x04, 0x82, 0x00, 0x27, 0x73, 0x12, 0x9e, 0x58, 0x64, 0xb5, 0x7b, 0x41, 0xfb, 0xca, 0xdb, 0x9d, 0x0b, 0xd5, 0x3f, 0x9d, 0x70, 0xb0, 0x23, 0x71, 0xe8, 0xc7, 0xfd, 0x6b, 0x7f, 0xfe, 0x30, 0x5f, 0x14, 0x47, 0x9e, 0x2f, 0x01, 0x04, 0x00, 0x62, 0x65, 0x74, 0x61, ];
pub fn build_tag(magic: &[u8], version: u8, length_field: u16, channel: &[u8], outer_len: Option<u16>) -> Vec<u8> {
let mut record = Vec::new();
record.extend_from_slice(magic);
record.push(version);
record.extend_from_slice(&length_field.to_le_bytes());
record.extend_from_slice(channel);
let n = outer_len.unwrap_or(record.len() as u16);
let mut blob = super::OID_SEARCH_BYTES.to_vec();
blob.extend_from_slice(&n.to_be_bytes());
blob.extend_from_slice(&record);
blob
}
pub fn valid_tag(channel: &str) -> Vec<u8> {
build_tag(&super::CHANNEL_TAG_MAGIC, super::FORMAT_VERSION, channel.len() as u16, channel.as_bytes(), None)
}
pub fn embed_in_junk(tag: &[u8]) -> Vec<u8> {
let mut sig = vec![0x30, 0x82, 0x10, 0x00, 0xde, 0xad, 0xbe, 0xef];
sig.extend_from_slice(tag);
sig.extend_from_slice(&[0x05, 0x00, 0xa0, 0x03, 0x02, 0x01, 0x02]);
sig
}
pub const SQ_VERSION_FIXTURE: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>MyTestApp</id>
<title>MyTestApp</title>
<description>MyTestApp</description>
<authors>MyTestApp</authors>
<version>1.0.11</version>
<channel>win</channel>
<mainExe>MyTestApp.exe</mainExe>
<os>win</os>
<rid>win-x64</rid>
</metadata>
</package>"#;
pub fn write_install_fixture(root: &Path) -> PathBuf {
let current = root.join("current");
fs::create_dir_all(¤t).unwrap();
let manifest = current.join("sq.version");
fs::write(&manifest, SQ_VERSION_FIXTURE).unwrap();
manifest
}
}
#[cfg(test)]
mod tests {
use super::test_support::*;
use super::*;
#[test]
fn golden_vector_round_trips() {
assert_eq!(read_channel_from_signature(&GOLDEN_VECTOR_BETA), Some("beta".to_string()));
assert_eq!(valid_tag("beta"), GOLDEN_VECTOR_BETA.to_vec());
}
#[test]
fn valid_tag_embedded_in_junk() {
assert_eq!(read_channel_from_signature(&embed_in_junk(&valid_tag("stable-2"))), Some("stable-2".to_string()));
}
#[test]
fn max_length_channel_is_accepted() {
let channel = "a".repeat(MAX_CHANNEL_LEN);
assert_eq!(read_channel_from_signature(&embed_in_junk(&valid_tag(&channel))), Some(channel));
}
#[test]
fn empty_input_returns_none() {
assert_eq!(read_channel_from_signature(&[]), None);
}
#[test]
fn absent_marker_returns_none() {
let sig = vec![0xffu8; 4096];
assert_eq!(read_channel_from_signature(&sig), None);
}
#[test]
fn wrong_oid_returns_none() {
let mut tag = valid_tag("beta");
tag[7] = 0xd7; assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn truncated_outer_length_returns_none() {
let mut sig = OID_SEARCH_BYTES.to_vec();
sig.push(0x00);
assert_eq!(read_channel_from_signature(&sig), None);
}
#[test]
fn outer_length_exceeding_remaining_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 4, b"beta", Some(1000));
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn outer_length_too_small_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 4, b"beta", Some(35));
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn wrong_magic_returns_none() {
let mut magic = CHANNEL_TAG_MAGIC;
magic[0] ^= 0xff;
let tag = build_tag(&magic, FORMAT_VERSION, 4, b"beta", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn wrong_version_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, 0x02, 4, b"beta", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn zero_length_channel_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 0, b"x", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn oversized_length_channel_returns_none() {
let channel = "a".repeat(65);
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 65, channel.as_bytes(), None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn length_exceeding_record_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 10, b"beta", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn uppercase_channel_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 4, b"Beta", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn channel_with_space_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 6, b"be ta ", None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn non_ascii_channel_returns_none() {
let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 5, &[0x62, 0x65, 0x74, 0xc3, 0xa4], None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), None);
}
#[test]
fn multiple_valid_tags_last_wins() {
let mut sig = embed_in_junk(&valid_tag("alpha"));
sig.extend_from_slice(&embed_in_junk(&valid_tag("beta")));
assert_eq!(read_channel_from_signature(&sig), Some("beta".to_string()));
}
#[test]
fn invalid_tag_then_valid_tag_returns_valid() {
let bad = build_tag(&CHANNEL_TAG_MAGIC, 0x7f, 4, b"nope", None);
let mut sig = embed_in_junk(&bad);
sig.extend_from_slice(&embed_in_junk(&valid_tag("stable")));
assert_eq!(read_channel_from_signature(&sig), Some("stable".to_string()));
}
#[test]
fn valid_tag_then_invalid_tag_returns_valid() {
let bad = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 4, b"NOPE", None);
let mut sig = embed_in_junk(&valid_tag("stable"));
sig.extend_from_slice(&embed_in_junk(&bad));
assert_eq!(read_channel_from_signature(&sig), Some("stable".to_string()));
}
#[test]
fn trailing_bytes_after_record_are_ignored() {
let mut record_channel = b"beta".to_vec();
record_channel.extend_from_slice(&[0x00, 0x00]); let tag = build_tag(&CHANNEL_TAG_MAGIC, FORMAT_VERSION, 4, &record_channel, None);
assert_eq!(read_channel_from_signature(&embed_in_junk(&tag)), Some("beta".to_string()));
}
fn make_install_fixture() -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_install_fixture(tmp.path());
(tmp, manifest)
}
#[test]
fn patch_rewrites_channel_element_only() {
let (tmp, manifest) = make_install_fixture();
patch_installed_channel(tmp.path(), "beta").unwrap();
let patched = fs::read_to_string(&manifest).unwrap();
assert_eq!(patched, SQ_VERSION_FIXTURE.replace("<channel>win</channel>", "<channel>beta</channel>"));
assert!(!manifest.with_extension("version.tmp").exists());
}
#[test]
fn patch_is_idempotent_for_same_channel() {
let (tmp, manifest) = make_install_fixture();
patch_installed_channel(tmp.path(), "win").unwrap();
assert_eq!(fs::read_to_string(&manifest).unwrap(), SQ_VERSION_FIXTURE);
}
#[test]
fn patch_rejects_invalid_channels() {
let (tmp, manifest) = make_install_fixture();
for bad in ["", "Beta", "be ta", "beta\n", "../evil", "b\\eta", "b/eta", &"a".repeat(65), "bét"] {
assert!(patch_installed_channel(tmp.path(), bad).is_err(), "channel {:?} should be rejected", bad);
}
assert_eq!(fs::read_to_string(&manifest).unwrap(), SQ_VERSION_FIXTURE);
}
#[test]
fn patch_errors_when_manifest_missing() {
let tmp = tempfile::tempdir().unwrap();
assert!(patch_installed_channel(tmp.path(), "beta").is_err());
}
#[test]
fn patch_errors_when_channel_element_missing() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join("current");
fs::create_dir_all(¤t).unwrap();
fs::write(current.join("sq.version"), "<package><metadata><id>x</id></metadata></package>").unwrap();
assert!(patch_installed_channel(tmp.path(), "beta").is_err());
}
}