rusty-javac 0.2.3

A Java compiler written in Rust.
Documentation
#[path = "classpath/source.rs"]
mod source;

use self::source::JavaSource;
use crate::call_resolver::ClassCatalog;
use crate::ty::descriptor::{descriptor_to_ty, method_descriptor_to_sig};
use rust_asm::class_reader::read_class_file;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use zip::ZipArchive;

const ACC_INTERFACE: u16 = 0x0200;

pub(crate) fn build_class_catalog(
    classpath: &[String],
    source_files: &[String],
) -> Result<ClassCatalog, Vec<String>> {
    let mut scanner = ClasspathScanner {
        catalog: ClassCatalog::platform(),
        errors: Vec::new(),
        sources: Vec::new(),
    };

    for source_file in source_files {
        scanner.register_primary_source(Path::new(source_file));
    }

    for entry in classpath_entries(classpath) {
        scanner.scan_classpath_entry(&entry);
    }

    scanner.register_source_members();

    if scanner.errors.is_empty() {
        Ok(scanner.catalog)
    } else {
        Err(scanner.errors)
    }
}

struct ClasspathScanner {
    catalog: ClassCatalog,
    errors: Vec<String>,
    sources: Vec<JavaSource>,
}

impl ClasspathScanner {
    fn register_primary_source(&mut self, path: &Path) {
        if let Ok(source) = fs::read_to_string(path) {
            self.register_java_source(path.display().to_string(), source);
        }
    }

    fn scan_classpath_entry(&mut self, path: &Path) {
        if !path.exists() {
            self.errors
                .push(format!("classpath entry not found: {}", path.display()));
            return;
        }

        if path.is_dir() {
            self.scan_directory(path, path);
        } else {
            self.scan_file(path, None);
        }
    }

    fn scan_directory(&mut self, root: &Path, directory: &Path) {
        let entries = match fs::read_dir(directory) {
            Ok(entries) => entries,
            Err(error) => {
                self.errors.push(format!(
                    "failed to read classpath directory {}: {}",
                    directory.display(),
                    error
                ));
                return;
            }
        };

        for entry in entries {
            match entry {
                Ok(entry) => {
                    let path = entry.path();
                    if path.is_dir() {
                        self.scan_directory(root, &path);
                    } else {
                        let fallback = fallback_internal_name(root, &path);
                        self.scan_file(&path, fallback);
                    }
                }
                Err(error) => self.errors.push(format!(
                    "failed to read entry in {}: {}",
                    directory.display(),
                    error
                )),
            }
        }
    }

    fn scan_file(&mut self, path: &Path, fallback_internal_name: Option<String>) {
        match extension(path).as_deref() {
            Some("class") => self.register_class_file(path, fallback_internal_name),
            Some("jar") => self.scan_jar(path),
            Some("java") => self.register_classpath_source(path),
            _ => {}
        }
    }

    fn register_classpath_source(&mut self, path: &Path) {
        match fs::read_to_string(path) {
            Ok(source) => self.register_java_source(path.display().to_string(), source),
            Err(error) => self.errors.push(format!(
                "failed to read classpath source {}: {}",
                path.display(),
                error
            )),
        }
    }

    fn register_class_file(&mut self, path: &Path, fallback_internal_name: Option<String>) {
        match fs::read(path) {
            Ok(bytes) => self.register_class_bytes(
                &path.display().to_string(),
                &bytes,
                fallback_internal_name,
            ),
            Err(error) => self.errors.push(format!(
                "failed to read class file {}: {}",
                path.display(),
                error
            )),
        }
    }

    fn scan_jar(&mut self, path: &Path) {
        let file = match fs::File::open(path) {
            Ok(file) => file,
            Err(error) => {
                self.errors
                    .push(format!("failed to open jar {}: {}", path.display(), error));
                return;
            }
        };
        let mut archive = match ZipArchive::new(file) {
            Ok(archive) => archive,
            Err(error) => {
                self.errors
                    .push(format!("failed to read jar {}: {}", path.display(), error));
                return;
            }
        };

        for index in 0..archive.len() {
            let mut entry = match archive.by_index(index) {
                Ok(entry) => entry,
                Err(error) => {
                    self.errors.push(format!(
                        "failed to read entry {} from jar {}: {}",
                        index,
                        path.display(),
                        error
                    ));
                    continue;
                }
            };
            let entry_name = entry.name().replace('\\', "/");
            if entry_name.ends_with(".class") {
                let mut bytes = Vec::new();
                if let Err(error) = entry.read_to_end(&mut bytes) {
                    self.errors.push(format!(
                        "failed to read class {} from jar {}: {}",
                        entry_name,
                        path.display(),
                        error
                    ));
                    continue;
                }
                let fallback = entry_name.strip_suffix(".class").map(str::to_string);
                self.register_class_bytes(
                    &format!("{}!{}", path.display(), entry_name),
                    &bytes,
                    fallback,
                );
            } else if entry_name.ends_with(".java") {
                let mut source = String::new();
                if let Err(error) = entry.read_to_string(&mut source) {
                    self.errors.push(format!(
                        "failed to read source {} from jar {}: {}",
                        entry_name,
                        path.display(),
                        error
                    ));
                    continue;
                }
                self.register_java_source(format!("{}!{}", path.display(), entry_name), source);
            }
        }
    }

    fn register_class_bytes(
        &mut self,
        label: &str,
        bytes: &[u8],
        fallback_internal_name: Option<String>,
    ) {
        match read_class_file(bytes) {
            Ok(class_file) => match class_file.class_name(class_file.this_class) {
                Ok(internal_name) => {
                    let internal_name = internal_name.to_string();
                    self.catalog.insert_internal_class(&internal_name);
                    if class_file.access_flags & ACC_INTERFACE != 0 {
                        self.catalog.mark_interface(&internal_name);
                    }
                    self.register_class_parents(&internal_name, &class_file);
                    self.register_class_members(&internal_name, &class_file);
                }
                Err(error) => self.handle_class_read_error(label, fallback_internal_name, error),
            },
            Err(error) => {
                self.handle_class_read_error(label, fallback_internal_name, error);
            }
        }
    }

    fn handle_class_read_error(
        &mut self,
        label: &str,
        fallback_internal_name: Option<String>,
        error: impl std::fmt::Display,
    ) {
        if let Some(internal_name) = fallback_internal_name {
            self.catalog.insert_internal_class(internal_name);
        } else {
            self.errors.push(format!(
                "failed to read class metadata from {label}: {error}"
            ));
        }
    }

    fn register_class_members(
        &mut self,
        internal_name: &str,
        class_file: &rust_asm::class_reader::ClassFile,
    ) {
        let is_interface = class_file.access_flags & ACC_INTERFACE != 0;
        for field in &class_file.fields {
            let Ok(name) = class_file.cp_utf8(field.name_index) else {
                continue;
            };
            let Ok(descriptor) = class_file.cp_utf8(field.descriptor_index) else {
                continue;
            };
            if let Some(ty) = descriptor_to_ty(descriptor) {
                self.catalog
                    .insert_field(internal_name, name, descriptor, ty, field.access_flags);
            }
        }

        for method in &class_file.methods {
            let Ok(name) = class_file.cp_utf8(method.name_index) else {
                continue;
            };
            let Ok(descriptor) = class_file.cp_utf8(method.descriptor_index) else {
                continue;
            };
            if let Some(sig) = method_descriptor_to_sig(name, descriptor) {
                self.catalog
                    .insert_method(internal_name, sig, method.access_flags, is_interface);
            }
        }
    }

    fn register_class_parents(
        &mut self,
        internal_name: &str,
        class_file: &rust_asm::class_reader::ClassFile,
    ) {
        if class_file.super_class != 0
            && let Ok(super_name) = class_file.class_name(class_file.super_class)
        {
            self.catalog.insert_parent(internal_name, super_name);
        }
        for interface in &class_file.interfaces {
            if let Ok(interface_name) = class_file.class_name(*interface) {
                self.catalog.insert_parent(internal_name, interface_name);
            }
        }
    }

    fn register_java_source(&mut self, label: String, source: String) {
        for internal_name in source::type_names(&source) {
            self.catalog.insert_internal_class(internal_name);
        }
        self.sources.push(JavaSource::new(label, source));
    }

    fn register_source_members(&mut self) {
        source::register_members(&mut self.catalog, &mut self.errors, &self.sources);
    }
}

fn extension(path: &Path) -> Option<String> {
    path.extension()
        .and_then(|extension| extension.to_str())
        .map(|extension| extension.to_ascii_lowercase())
}

fn fallback_internal_name(root: &Path, path: &Path) -> Option<String> {
    if extension(path).as_deref() != Some("class") {
        return None;
    }

    let relative_path = path.strip_prefix(root).ok()?;
    let mut internal_name = PathBuf::new();
    internal_name.push(relative_path);
    internal_name.set_extension("");
    Some(path_to_internal_name(&internal_name))
}

fn path_to_internal_name(path: &Path) -> String {
    path.components()
        .filter_map(|component| component.as_os_str().to_str())
        .collect::<Vec<_>>()
        .join("/")
}

fn classpath_entries(classpath: &[String]) -> Vec<PathBuf> {
    classpath
        .iter()
        .flat_map(|entry| std::env::split_paths(entry))
        .filter(|path| !path.as_os_str().is_empty())
        .collect()
}