blat 0.1.1

bluetooth git remote helper
use bluer::{
    Address, AddressType,
    l2cap::{PSM_LE_DYN_START, SocketAddr, Stream},
};
use std::{
    env,
    fs::{OpenOptions, create_dir_all},
    io::{Write, stdin},
    path::PathBuf,
};
use tokio::io::{AsyncBufReadExt, AsyncReadExt};
use tokio::io::{AsyncWriteExt, BufStream};

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 args: Vec<_> = env::args().collect();
    assert!(
        args.len() >= 2,
        "Specify target Bluetooth address as argument"
    );
    let last_arg = args.last().unwrap();
    let target_addr: Address = last_arg
        .strip_prefix("blat::")
        .unwrap_or(last_arg)
        .parse()
        .unwrap();
    let sa = SocketAddr::new(target_addr, AddressType::LePublic, PSM);
    let stream = Stream::connect(sa).await.unwrap();
    let mut bs = BufStream::new(stream);
    bs.read_u8().await.unwrap();

    let mut line = String::new();
    loop {
        line.clear();
        stdin().read_line(&mut line).unwrap();
        if line.is_empty() {
            break;
        }
        let tokens = line
            .trim()
            .split(' ')
            .filter(|s| !s.is_empty())
            .map(str::trim)
            .collect::<Vec<_>>();
        match tokens.as_slice() {
            ["capabilities"] => {
                println!("fetch");
                println!();
            }
            ["list"] => {
                bs.write_all("list\n".as_bytes()).await.unwrap();
                bs.flush().await.unwrap();
                let list = list_response(&mut bs).await;
                for item in list {
                    println!("{item}");
                }
                println!();
            }
            ["fetch", id, _name] => {
                bs.write_all(format!("fetch {id}\n").as_bytes())
                    .await
                    .unwrap();
                bs.flush().await.unwrap();
                fetch_response(&mut bs).await;
                println!();
            }
            _ => {}
        }
    }
    bs.write_all("\n".as_bytes()).await.unwrap();
}

async fn list_response(bs: &mut BufStream<Stream>) -> Vec<String> {
    let mut line = String::new();
    let mut res = vec![];
    loop {
        line.clear();
        bs.read_line(&mut line).await.unwrap();
        if line.trim().is_empty() {
            break;
        }
        res.push(line.trim().to_string());
    }
    res
}

async fn fetch_response(bs: &mut BufStream<Stream>) {
    let dst = env::var("GIT_DIR").map(PathBuf::from).unwrap();
    let mut line = String::new();
    loop {
        line.clear();
        bs.read_line(&mut line).await.unwrap();
        if line.trim().is_empty() {
            break;
        }
        let tokens = line
            .trim()
            .split(' ')
            .filter(|s| !s.is_empty())
            .map(str::trim)
            .collect::<Vec<_>>();
        match tokens.as_slice() {
            [id, size] => {
                let mut buf = vec![0; size.parse().unwrap()];
                let mut dst = dst.clone();
                bs.read_exact(buf.as_mut_slice()).await.unwrap();
                dst.push("objects");
                dst.push(&id[..2]);
                create_dir_all(&dst).unwrap();
                dst.push(&id[2..]);
                if dst.exists() {
                    return;
                }
                let mut f = OpenOptions::new()
                    .write(true)
                    .truncate(true)
                    .create(true)
                    .open(dst)
                    .unwrap();
                f.write_all(buf.as_slice()).unwrap();
            }
            _ => panic!("Bad fetch response tokens"),
        }
    }
}