blat 0.1.1

bluetooth git remote helper
use bluer::{
    Uuid,
    adv::Advertisement,
    l2cap::{PSM_LE_DYN_START, SocketAddr, Stream, StreamListener},
};
use std::{env::current_dir, error::Error, fs::File, io::Read, process::Command};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufStream};

const SERVICE_UUID: Uuid = Uuid::from_u128(0x18c2_12e8_657d_4228_be09_ce01_bcc0_3e13);
const PSM: u16 = PSM_LE_DYN_START + 5;

#[tokio::main]
async fn main() {
    let session = bluer::Session::new().await.unwrap();
    let adapter = session.default_adapter().await.unwrap();
    adapter.set_powered(true).await.unwrap();
    let adapter_addr = adapter.address().await.unwrap();
    let adapter_addr_type = adapter.address_type().await.unwrap();
    let le_advertisement = Advertisement {
        service_uuids: vec![SERVICE_UUID].into_iter().collect(),
        discoverable: Some(true),
        local_name: Some("blat".to_string()),
        ..Default::default()
    };
    let _adv_handle = adapter.advertise(le_advertisement).await.unwrap();

    let local_sa = SocketAddr::new(adapter_addr, adapter_addr_type, PSM);
    let mut listener = StreamListener::bind(local_sa).await.unwrap();

    loop {
        eprintln!(
            "Waiting for connection, remote url: 'blat::{}'",
            local_sa.addr
        );
        if let Err(e) = handle_connection(&mut listener).await {
            eprintln!("{e}");
        }
    }
}

async fn handle_connection(listener: &mut StreamListener) -> Result<(), Box<dyn Error>> {
    let (stream, _) = listener.accept().await?;
    let mut bs = BufStream::new(stream);
    bs.write_u8(255).await?;
    bs.flush().await?;
    eprintln!("Accepted connection.");

    let mut line = String::new();
    loop {
        line.clear();
        bs.read_line(&mut line).await?;
        if line.is_empty() {
            return Ok(());
        }
        let tokens = line
            .split(' ')
            .filter(|s| !s.is_empty())
            .map(str::trim)
            .collect::<Vec<_>>();
        eprintln!("Received: {tokens:?}");
        match tokens.as_slice() {
            ["list"] => list_refs(&mut bs).await?,
            ["fetch", id] => write_fetch(&mut bs, id).await?,
            _ => return Ok(()),
        }
    }
}

async fn list_refs(bs: &mut BufStream<Stream>) -> Result<(), Box<dyn Error>> {
    let out = Command::new("git")
        .arg("show-ref")
        .current_dir(current_dir()?)
        .output()?
        .stdout;
    bs.write_all(out.as_slice()).await?;
    bs.write_all("\n".as_bytes()).await?;
    bs.flush().await?;
    Ok(())
}

async fn write_fetch(bs: &mut BufStream<Stream>, id: &str) -> Result<(), Box<dyn Error>> {
    let cmdout = &Command::new("git")
        .arg("rev-list")
        .arg("--objects")
        .arg(id)
        .current_dir(current_dir()?)
        .output()?
        .stdout;
    let cmdoutstr = str::from_utf8(cmdout)?;
    eprintln!("Expanded {id} to: {cmdoutstr}\n");
    let ids = cmdoutstr
        .split('\n')
        .map(str::trim)
        .collect::<Vec<_>>();
    for id in ids {
        if id.trim().is_empty() {
            continue;
        }
        let id = id.split(' ').next().unwrap();
        let mut src = current_dir()?.as_path().to_path_buf();
        src.push(".git");
        src.push("objects");
        src.push(&id[..2]);
        src.push(&id[2..]);
        if !src.exists() {
            return Ok(());
        }
        let mut buf = vec![];
        let src = src.as_path();
        File::open(src)?.read_to_end(&mut buf)?;
        eprintln!("Sending fetch of {id} {} bytes...", buf.len());
        bs.write_all(format!("{id} {}\n", buf.len()).as_bytes())
            .await?;
        bs.flush().await?;
        bs.write_all(buf.as_slice()).await?;
        bs.flush().await?;
        eprintln!("Sent fetch of {id} {} bytes", buf.len());
    }
    bs.write_all("\n".as_bytes()).await?;
    bs.flush().await?;
    Ok(())
}