use anyhow::{Context, Result};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::incremental::walk_files;
pub fn class_source_file(class_path: &Path) -> Option<String> {
let bytes = std::fs::read(class_path).ok()?;
let mut r = Reader::new(&bytes);
if r.read_u32()? != 0xCAFEBABE {
return None;
}
let _minor = r.read_u16()?;
let _major = r.read_u16()?;
let cp_count = r.read_u16()?;
let mut utf8: std::collections::HashMap<u16, &[u8]> = std::collections::HashMap::new();
let mut idx: u16 = 1;
while idx < cp_count {
let tag = r.read_u8()?;
match tag {
1 => {
let len = r.read_u16()? as usize;
let raw = r.read_bytes(len)?;
utf8.insert(idx, raw);
}
3 | 4 => {
r.skip(4)?;
}
5 | 6 => {
r.skip(8)?;
idx = idx.checked_add(1)?;
}
7 | 8 | 16 | 19 | 20 => {
r.skip(2)?;
}
9 | 10 | 11 | 12 | 17 | 18 => {
r.skip(4)?;
}
15 => {
r.skip(3)?;
}
_ => return None,
}
idx = idx.checked_add(1)?;
}
r.skip(6)?;
let ifc = r.read_u16()? as usize;
r.skip(2 * ifc)?;
let fields_count = r.read_u16()? as usize;
for _ in 0..fields_count {
r.skip(6)?; skip_attributes(&mut r)?;
}
let methods_count = r.read_u16()? as usize;
for _ in 0..methods_count {
r.skip(6)?;
skip_attributes(&mut r)?;
}
let class_attr_count = r.read_u16()? as usize;
for _ in 0..class_attr_count {
let name_idx = r.read_u16()?;
let alen = r.read_u32()? as usize;
if utf8.get(&name_idx).copied() == Some(b"SourceFile".as_ref()) {
if alen != 2 {
return None;
}
let src_idx = r.read_u16()?;
let raw = utf8.get(&src_idx).copied()?;
return Some(String::from_utf8_lossy(raw).into_owned());
}
r.skip(alen)?;
}
None
}
fn skip_attributes(r: &mut Reader) -> Option<()> {
let n = r.read_u16()? as usize;
for _ in 0..n {
r.skip(2)?; let alen = r.read_u32()? as usize;
r.skip(alen)?;
}
Some(())
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn read_u8(&mut self) -> Option<u8> {
let b = *self.bytes.get(self.pos)?;
self.pos += 1;
Some(b)
}
fn read_u16(&mut self) -> Option<u16> {
let hi = self.read_u8()? as u16;
let lo = self.read_u8()? as u16;
Some((hi << 8) | lo)
}
fn read_u32(&mut self) -> Option<u32> {
let a = self.read_u8()? as u32;
let b = self.read_u8()? as u32;
let c = self.read_u8()? as u32;
let d = self.read_u8()? as u32;
Some((a << 24) | (b << 16) | (c << 8) | d)
}
fn read_bytes(&mut self, n: usize) -> Option<&'a [u8]> {
let end = self.pos.checked_add(n)?;
let slice = self.bytes.get(self.pos..end)?;
self.pos = end;
Some(slice)
}
fn skip(&mut self, n: usize) -> Option<()> {
let end = self.pos.checked_add(n)?;
if end > self.bytes.len() {
return None;
}
self.pos = end;
Some(())
}
}
pub fn wipe_kotlin_derived_classes(classes_dir: &Path) -> Result<Vec<PathBuf>> {
if !classes_dir.exists() {
return Ok(Vec::new());
}
let mut removed = Vec::new();
for entry in walk_files(classes_dir) {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) != Some("class") {
continue;
}
if let Some(src) = class_source_file(p) {
if src.ends_with(".kt") {
std::fs::remove_file(p)
.with_context(|| format!("failed to remove stale {}", p.display()))?;
removed.push(p.to_path_buf());
}
}
}
Ok(removed)
}
pub fn kt_sources_stamp_path(target_dir: &Path) -> PathBuf {
target_dir.join(".kt-sources")
}
pub fn load_kt_sources(target_dir: &Path) -> Option<BTreeSet<String>> {
let path = kt_sources_stamp_path(target_dir);
let text = std::fs::read_to_string(&path).ok()?;
Some(
text.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect(),
)
}
pub fn write_kt_sources(target_dir: &Path, sources: &BTreeSet<String>) -> Result<()> {
let path = kt_sources_stamp_path(target_dir);
let body = {
let mut s = String::with_capacity(sources.iter().map(|p| p.len() + 1).sum());
for p in sources {
s.push_str(p);
s.push('\n');
}
s
};
std::fs::write(&path, body)
.with_context(|| format!("failed to write {}", path.display()))
}
pub fn canonical_kt_set(sources: &[PathBuf]) -> BTreeSet<String> {
sources
.iter()
.filter_map(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().into_owned())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn build_minimal_class(source_name: &str) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(&0xCAFEBABEu32.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&52u16.to_be_bytes());
out.extend_from_slice(&4u16.to_be_bytes());
let sf_bytes = b"SourceFile";
out.push(1);
out.extend_from_slice(&(sf_bytes.len() as u16).to_be_bytes());
out.extend_from_slice(sf_bytes);
let sn = source_name.as_bytes();
out.push(1);
out.extend_from_slice(&(sn.len() as u16).to_be_bytes());
out.extend_from_slice(sn);
out.push(7);
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&3u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&2u16.to_be_bytes());
out
}
fn write_class(path: &Path, source_name: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let mut f = std::fs::File::create(path).unwrap();
f.write_all(&build_minimal_class(source_name)).unwrap();
}
fn build_class_with_modified_utf8(source_name: &str) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(&0xCAFEBABEu32.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&52u16.to_be_bytes());
out.extend_from_slice(&5u16.to_be_bytes());
let sf = b"SourceFile";
out.push(1);
out.extend_from_slice(&(sf.len() as u16).to_be_bytes());
out.extend_from_slice(sf);
let blob: &[u8] = &[b'a', 0xC0, 0x80, b'b'];
out.push(1);
out.extend_from_slice(&(blob.len() as u16).to_be_bytes());
out.extend_from_slice(blob);
let sn = source_name.as_bytes();
out.push(1);
out.extend_from_slice(&(sn.len() as u16).to_be_bytes());
out.extend_from_slice(sn);
out.push(7);
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&4u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&1u16.to_be_bytes());
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&3u16.to_be_bytes());
out
}
#[test]
fn parses_through_modified_utf8_entries() {
let dir = tempfile::tempdir().unwrap();
let c = dir.path().join("Mod.class");
std::fs::write(&c, &build_class_with_modified_utf8("Greeting.kt")).unwrap();
assert_eq!(class_source_file(&c).as_deref(), Some("Greeting.kt"));
}
#[test]
fn extracts_source_file_for_kotlin() {
let dir = tempfile::tempdir().unwrap();
let c = dir.path().join("Foo.class");
write_class(&c, "Foo.kt");
assert_eq!(class_source_file(&c).as_deref(), Some("Foo.kt"));
}
#[test]
fn extracts_source_file_for_java() {
let dir = tempfile::tempdir().unwrap();
let c = dir.path().join("Foo.class");
write_class(&c, "Foo.java");
assert_eq!(class_source_file(&c).as_deref(), Some("Foo.java"));
}
#[test]
fn missing_file_returns_none() {
assert!(class_source_file(Path::new("/nonexistent.class")).is_none());
}
#[test]
fn malformed_file_returns_none_safely() {
let dir = tempfile::tempdir().unwrap();
let c = dir.path().join("garbage.class");
std::fs::write(&c, b"not a class file at all").unwrap();
assert!(class_source_file(&c).is_none());
}
#[test]
fn truncated_file_returns_none_safely() {
let dir = tempfile::tempdir().unwrap();
let c = dir.path().join("short.class");
std::fs::write(&c, &0xCAFEBABEu32.to_be_bytes()).unwrap();
assert!(class_source_file(&c).is_none());
}
#[test]
fn wipe_removes_only_kotlin_derived_classes() {
let dir = tempfile::tempdir().unwrap();
let classes = dir.path().join("classes");
let kt_class = classes.join("com").join("FooKt.class");
let kt_extra = classes.join("com").join("Bar.class");
let java_class = classes.join("com").join("Baz.class");
write_class(&kt_class, "Foo.kt");
write_class(&kt_extra, "Foo.kt"); write_class(&java_class, "Baz.java");
let removed = wipe_kotlin_derived_classes(&classes).unwrap();
assert_eq!(removed.len(), 2);
assert!(!kt_class.exists());
assert!(!kt_extra.exists());
assert!(java_class.exists(), "Java-derived class must survive wipe");
}
#[test]
fn wipe_no_classes_dir_returns_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(wipe_kotlin_derived_classes(&dir.path().join("ghost"))
.unwrap()
.is_empty());
}
#[test]
fn wipe_ignores_non_class_files() {
let dir = tempfile::tempdir().unwrap();
let classes = dir.path().join("classes");
std::fs::create_dir_all(&classes).unwrap();
let res = classes.join("META-INF").join("main.kotlin_module");
std::fs::create_dir_all(res.parent().unwrap()).unwrap();
std::fs::write(&res, b"resource").unwrap();
assert!(wipe_kotlin_derived_classes(&classes).unwrap().is_empty());
assert!(res.exists());
}
#[test]
fn kt_sources_round_trip() {
let dir = tempfile::tempdir().unwrap();
let mut set = BTreeSet::new();
set.insert("/a/Foo.kt".to_string());
set.insert("/a/Bar.kt".to_string());
write_kt_sources(dir.path(), &set).unwrap();
let read = load_kt_sources(dir.path()).unwrap();
assert_eq!(read, set);
}
#[test]
fn kt_sources_load_missing_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert!(load_kt_sources(dir.path()).is_none());
}
#[test]
fn kt_sources_ignores_blank_lines_and_whitespace() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
kt_sources_stamp_path(dir.path()),
"\n /a/Foo.kt \n\n/a/Bar.kt\n",
)
.unwrap();
let read = load_kt_sources(dir.path()).unwrap();
let mut expected = BTreeSet::new();
expected.insert("/a/Foo.kt".to_string());
expected.insert("/a/Bar.kt".to_string());
assert_eq!(read, expected);
}
}