1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#![deny(missing_docs)]
//! High-level Rust API for working with the kpathsea file-searching library for TeX

use kpathsea_sys::*;
use std::ffi::{CStr,CString};
use which::which;

/// External result type for handling library errors
pub type Result<T> = std::result::Result<T, &'static str>;

/// High-level interface struct for the kpathsea API
pub struct Kpaths(kpathsea);

// A kpathsea pointer is Send because it owns the data that it references. It
// is not Sync, because calling kpathsea functions on it is not thread-safe.
unsafe impl Send for Kpaths {}

/// Returns the path to the kpsewhich executable on the system.
fn get_kpsewhich_path() -> Result<CString> {
  let kpsewhich_path = which("kpsewhich")
    .map_err(|_| "Error finding kpsewhich executable")?;
  let kpsewhich_path_str = kpsewhich_path.to_string_lossy();
  Ok(CString::new(kpsewhich_path_str.into_owned().as_str()).unwrap())
}

impl Kpaths {
  /// Obtain a new kpathsea struct, with metadata for the current rust executable
  pub fn new() -> Result<Self> {
    let kpse = unsafe { kpathsea_new() };

    // kpathsea says we should pass in the current executable name to
    // kpathsea_set_program_name, but there are cases where this causes
    // kpathsea to fail to find the available TeX distribution. Instead, we use
    // the location of the kpsewhich executable, which ensures that we find the
    // correct TeX distribution.
    let kpsewhich_path = get_kpsewhich_path()?;

    unsafe {
      kpathsea_set_program_name(
        kpse,
        kpsewhich_path.as_ptr(),
        std::ptr::null()
      )
    }
    Ok(Kpaths(kpse))
  }

  /// For a given filename, try to guess the kpse format type from the file
  /// extension by looking it up in the format info table. This is a simplified
  /// version of the find_format function in kpsewhich.
  fn guess_format_from_filename(&self, filename: &str) -> kpse_file_format_type {
    // We go through each format type
    for format_type in 0..kpse_file_format_type_kpse_last_format {
      let format_info: &mut kpse_format_info_type =
        unsafe { &mut (*self.0).format_info[format_type as usize] };
      if format_info.type_.is_null() {
        // If this format hasn't been initialized yet, initialize it now.
        // Otherwise, it won't have the list of suffixes initialized.
        unsafe {
          kpathsea_init_format(self.0, format_type as kpse_file_format_type);
        }
      }

      // First, we check the suffixes for each format type. The suffixes are
      // stored as an array of strings with a null pointer denoting the last
      // value. Also, the pointer to the array can itself be null if there are
      // no suffixes.
      let mut suffix_ptr = format_info.suffix;
      while !suffix_ptr.is_null() && !unsafe {*suffix_ptr}.is_null() {
        // Pull out the suffix
        let suffix_cstr = unsafe { CStr::from_ptr(*suffix_ptr) };
        let suffix = suffix_cstr.to_str().unwrap();

        // We check if the last suffix.len() characters of the filename are
        // equal to the suffix itself. If so, then we've found a type that
        // matches our filename!
        if filename.get(filename.len()-suffix.len()..) == Some(suffix) {
          return format_type as kpse_file_format_type;
        }

        // Go to the next suffix in the array.
        suffix_ptr = unsafe { suffix_ptr.offset(1) };
      }

      // Next, we check the alternate suffixes for each format type. This is
      // stored in the exact same way as the normal suffixes.
      // TODO(xymostech): factor this out into a function to avoid duplication
      let mut alt_suffix_ptr = format_info.alt_suffix;
      while !alt_suffix_ptr.is_null() && !unsafe {*alt_suffix_ptr}.is_null() {
        let alt_suffix_cstr = unsafe { CStr::from_ptr(*alt_suffix_ptr) };
        let alt_suffix = alt_suffix_cstr.to_str().unwrap();

        if filename.get(filename.len()-alt_suffix.len()..) == Some(alt_suffix) {
          return format_type as kpse_file_format_type;
        }

        alt_suffix_ptr = unsafe { alt_suffix_ptr.offset(1) };
      }
    }

    // If we don't find any matching suffixes, we guess that it's a tex file
    kpse_file_format_type_kpse_tex_format
  }

  /// Find a file base name, auto-completing with the standard TeX extensions if needed
  pub fn find_file(&self, name: &str) -> Option<String> {
    let c_name = CString::new(name).unwrap();

    let file_format_type = self.guess_format_from_filename(name);
    let c_filename_buf = unsafe { kpathsea_find_file(
      self.0,
      c_name.as_ptr(),
      file_format_type,
      0
    )};
    
    if !c_filename_buf.is_null() {
      let c_filepath: &CStr = unsafe { CStr::from_ptr(c_filename_buf) };
      let filepath = c_filepath.to_str().unwrap().to_owned();
      if filepath.is_empty() { 
        None
      } else {
        Some(filepath)
      }
    } else {
      None
    }
  }
}

impl Drop for Kpaths {
  /// Cleanup the kpathsea pointer in the destructor
  fn drop(&mut self) {
    unsafe { kpathsea_finish(self.0) };
  }
}