#![deny(missing_docs)]
use flate2::{read::GzDecoder, Compression, GzBuilder};
use md5::{Digest, Md5};
use std::{
convert::TryInto,
ffi::CStr,
fmt::{Display, Error as FmtError, Formatter},
io::{self, Read, SeekFrom, Write},
path::PathBuf,
ptr,
result::Result as StdResult,
slice,
sync::Mutex,
};
use tectonic_errors::prelude::*;
use tectonic_io_base::{
digest::DigestData, normalize_tex_path, InputFeatures, InputHandle, IoProvider, OpenResult,
OutputHandle,
};
use tectonic_status_base::{tt_error, tt_warning, MessageKind, StatusBackend};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemRequestError {
NotImplemented,
NotAllowed,
Failed,
}
impl Display for SystemRequestError {
fn fmt(&self, f: &mut Formatter) -> StdResult<(), FmtError> {
write!(
f,
"{}",
match self {
SystemRequestError::NotImplemented => "not implemented by this driver",
SystemRequestError::NotAllowed => "not allowed by this driver",
SystemRequestError::Failed => "execution of the request failed",
}
)
}
}
pub trait DriverHooks {
fn io(&mut self) -> &mut dyn IoProvider;
fn event_output_closed(
&mut self,
_name: String,
_digest: DigestData,
_status: &mut dyn StatusBackend,
) {
}
fn event_input_closed(
&mut self,
_name: String,
_digest: Option<DigestData>,
_status: &mut dyn StatusBackend,
) {
}
fn sysrq_shell_escape(
&mut self,
_command: &str,
_status: &mut dyn StatusBackend,
) -> StdResult<(), SystemRequestError> {
Err(SystemRequestError::NotImplemented)
}
}
#[derive(Clone, Debug, Default)]
pub struct MinimalDriver<T: IoProvider>(T);
impl<T: IoProvider> MinimalDriver<T> {
pub fn new(io: T) -> Self {
MinimalDriver(io)
}
}
impl<T: IoProvider> DriverHooks for MinimalDriver<T> {
fn io(&mut self) -> &mut dyn IoProvider {
&mut self.0
}
}
extern "C" {
fn _ttbc_get_error_message() -> *const libc::c_char;
}
lazy_static::lazy_static! {
static ref ENGINE_LOCK: Mutex<u8> = Mutex::new(0u8);
}
#[derive(Debug)]
pub struct EngineAbortedError {
message: String,
}
impl EngineAbortedError {
pub fn new_abort_indicator() -> Self {
EngineAbortedError {
message: "[failed to extract detailed error message]".to_owned(),
}
}
unsafe fn new_with_details() -> Self {
let ptr = _ttbc_get_error_message();
let message = CStr::from_ptr(ptr).to_string_lossy().into_owned();
EngineAbortedError { message }
}
}
impl Display for EngineAbortedError {
fn fmt(&self, f: &mut Formatter) -> StdResult<(), FmtError> {
write!(f, "{}", self.message)
}
}
impl std::error::Error for EngineAbortedError {}
pub struct CoreBridgeLauncher<'a> {
hooks: &'a mut dyn DriverHooks,
status: &'a mut dyn StatusBackend,
security: SecuritySettings,
}
impl<'a> CoreBridgeLauncher<'a> {
pub fn new(hooks: &'a mut dyn DriverHooks, status: &'a mut dyn StatusBackend) -> Self {
Self::new_with_security(hooks, status, SecuritySettings::default())
}
pub fn new_with_security(
hooks: &'a mut dyn DriverHooks,
status: &'a mut dyn StatusBackend,
security: SecuritySettings,
) -> Self {
CoreBridgeLauncher {
hooks,
status,
security,
}
}
pub fn with_global_lock<F, T>(&mut self, callback: F) -> Result<T>
where
F: FnOnce(&mut CoreBridgeState<'_>) -> Result<T>,
{
let _guard = ENGINE_LOCK.lock().unwrap();
let mut state = CoreBridgeState::new(self.security.clone(), self.hooks, self.status);
let result = callback(&mut state);
if let Err(ref e) = result {
if e.downcast_ref::<EngineAbortedError>().is_some() {
return Err(unsafe { EngineAbortedError::new_with_details() }.into());
}
}
result
}
}
pub struct CoreBridgeState<'a> {
security: SecuritySettings,
hooks: &'a mut dyn DriverHooks,
status: &'a mut dyn StatusBackend,
#[allow(clippy::vec_box)]
input_handles: Vec<Box<InputHandle>>,
#[allow(clippy::vec_box)]
output_handles: Vec<Box<OutputHandle>>,
latest_input_path: Option<PathBuf>,
}
impl<'a> CoreBridgeState<'a> {
fn new(
security: SecuritySettings,
hooks: &'a mut dyn DriverHooks,
status: &'a mut dyn StatusBackend,
) -> CoreBridgeState<'a> {
CoreBridgeState {
security,
hooks,
status,
output_handles: Vec::new(),
input_handles: Vec::new(),
latest_input_path: None,
}
}
fn input_open_name_format(
&mut self,
name: &str,
format: FileFormat,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
let io = self.hooks.io();
if let FileFormat::Format = format {
match io.input_open_format(name, self.status) {
OpenResult::NotAvailable => {}
OpenResult::Err(e) => return OpenResult::Err(e),
OpenResult::Ok(h) => return OpenResult::Ok((h, None)),
}
} else {
match io.input_open_name_with_abspath(name, self.status) {
OpenResult::NotAvailable => {}
r => return r,
}
}
for e in format.extensions() {
let ext = format!("{}.{}", name, e);
if let FileFormat::Format = format {
match io.input_open_format(&ext, self.status) {
OpenResult::NotAvailable => {}
OpenResult::Err(e) => return OpenResult::Err(e),
OpenResult::Ok(h) => return OpenResult::Ok((h, None)),
}
} else {
match io.input_open_name_with_abspath(&ext, self.status) {
OpenResult::NotAvailable => {}
r => return r,
}
}
}
OpenResult::NotAvailable
}
fn input_open_name_format_gz(
&mut self,
name: &str,
format: FileFormat,
is_gz: bool,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
let base = self.input_open_name_format(name, format);
if !is_gz {
return base;
}
match base {
OpenResult::Ok((ih, path)) => {
let origin = ih.origin();
let dr = GzDecoder::new(ih.into_inner());
OpenResult::Ok((InputHandle::new(name, dr, origin), path))
}
_ => base,
}
}
fn get_file_md5(&mut self, name: &str, dest: &mut [u8]) -> bool {
let name = normalize_tex_path(name);
let mut hash = Md5::default();
let mut ih = match self.input_open_name_format(&name, FileFormat::Tex) {
OpenResult::Ok((ih, _path)) => ih,
OpenResult::NotAvailable => {
return true;
}
OpenResult::Err(e) => {
tt_error!(self.status, "error trying to open file \"{}\" for MD5 calculation",
name; e);
return true;
}
};
const BUF_SIZE: usize = 1024;
let mut buf = [0u8; BUF_SIZE];
let mut error_occurred = false;
loop {
let nread = match ih.read(&mut buf) {
Ok(0) => {
break;
}
Ok(n) => n,
Err(e) => {
tt_error!(self.status, "error reading file \"{}\" for MD5 calculation",
ih.name(); e.into());
error_occurred = true;
break;
}
};
hash.update(&buf[..nread]);
}
let (name, digest_opt) = ih.into_name_digest();
self.hooks.event_input_closed(name, digest_opt, self.status);
if !error_occurred {
let result = hash.finalize();
dest.copy_from_slice(result.as_slice());
}
error_occurred
}
fn output_open(&mut self, name: &str, is_gz: bool) -> *mut OutputHandle {
let io = self.hooks.io();
let name = normalize_tex_path(name);
let mut oh = match io.output_open_name(&name) {
OpenResult::Ok(oh) => oh,
OpenResult::NotAvailable => return ptr::null_mut(),
OpenResult::Err(e) => {
tt_warning!(self.status, "open of output {} failed", name; e);
return ptr::null_mut();
}
};
if is_gz {
let name = oh.name().to_owned();
oh = OutputHandle::new(
name,
GzBuilder::new().write(oh.into_inner(), Compression::default()),
);
}
self.output_handles.push(Box::new(oh));
&mut **self.output_handles.last_mut().unwrap()
}
fn output_open_stdout(&mut self) -> *mut OutputHandle {
let io = self.hooks.io();
let oh = match io.output_open_stdout() {
OpenResult::Ok(oh) => oh,
OpenResult::NotAvailable => return ptr::null_mut(),
OpenResult::Err(e) => {
tt_warning!(self.status, "open of stdout failed"; e);
return ptr::null_mut();
}
};
self.output_handles.push(Box::new(oh));
&mut **self.output_handles.last_mut().unwrap()
}
fn output_write(&mut self, handle: *mut OutputHandle, buf: &[u8]) -> bool {
let rhandle: &mut OutputHandle = unsafe { &mut *handle };
let result = rhandle.write_all(buf);
match result {
Ok(_) => false,
Err(e) => {
tt_warning!(self.status, "write failed"; e.into());
true
}
}
}
fn output_flush(&mut self, handle: *mut OutputHandle) -> bool {
let rhandle: &mut OutputHandle = unsafe { &mut *handle };
let result = rhandle.flush();
match result {
Ok(_) => false,
Err(e) => {
tt_warning!(self.status, "flush failed"; e.into());
true
}
}
}
fn output_close(&mut self, handle: *mut OutputHandle) -> bool {
let len = self.output_handles.len();
let mut rv = false;
for i in 0..len {
let p: *const OutputHandle = &*self.output_handles[i];
if p == handle {
let mut oh = self.output_handles.swap_remove(i);
if let Err(e) = oh.flush() {
tt_warning!(self.status, "error when closing output {}", oh.name(); e.into());
rv = true;
}
let (name, digest) = oh.into_name_digest();
self.hooks.event_output_closed(name, digest, self.status);
break;
}
}
rv
}
fn input_open(&mut self, name: &str, format: FileFormat, is_gz: bool) -> *mut InputHandle {
let name = normalize_tex_path(name);
let (ih, path) = match self.input_open_name_format_gz(&name, format, is_gz) {
OpenResult::Ok(tup) => tup,
OpenResult::NotAvailable => {
return ptr::null_mut();
}
OpenResult::Err(e) => {
tt_warning!(self.status, "open of input {} failed", name; e);
return ptr::null_mut();
}
};
self.input_handles.push(Box::new(ih));
self.latest_input_path = path;
&mut **self.input_handles.last_mut().unwrap()
}
fn input_open_primary(&mut self) -> *mut InputHandle {
let io = self.hooks.io();
let (ih, path) = match io.input_open_primary_with_abspath(self.status) {
OpenResult::Ok(tup) => tup,
OpenResult::NotAvailable => {
tt_error!(self.status, "primary input not available (?!)");
return ptr::null_mut();
}
OpenResult::Err(e) => {
tt_error!(self.status, "open of primary input failed"; e);
return ptr::null_mut();
}
};
self.input_handles.push(Box::new(ih));
self.latest_input_path = path;
&mut **self.input_handles.last_mut().unwrap()
}
fn input_get_size(&mut self, handle: *mut InputHandle) -> usize {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
match rhandle.get_size() {
Ok(s) => s,
Err(e) => {
tt_warning!(self.status, "failed to get the size of an input"; e);
0
}
}
}
fn input_get_mtime(&mut self, handle: *mut InputHandle) -> i64 {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
let maybe_time = match rhandle.get_unix_mtime() {
Ok(t) => t,
Err(e) => {
tt_warning!(self.status, "failed to get the modification time of an input"; e);
Some(0)
}
};
if let Some(t) = maybe_time {
t
} else {
1 }
}
fn input_seek(&mut self, handle: *mut InputHandle, pos: SeekFrom) -> Result<u64> {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
rhandle.try_seek(pos)
}
fn input_read(&mut self, handle: *mut InputHandle, buf: &mut [u8]) -> Result<()> {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
rhandle.read_exact(buf).map_err(Error::from)
}
fn input_getc(&mut self, handle: *mut InputHandle) -> Result<u8> {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
rhandle.getc()
}
fn input_ungetc(&mut self, handle: *mut InputHandle, byte: u8) -> Result<()> {
let rhandle: &mut InputHandle = unsafe { &mut *handle };
rhandle.ungetc(byte)
}
fn input_close(&mut self, handle: *mut InputHandle) -> bool {
let len = self.input_handles.len();
for i in 0..len {
let p: *const InputHandle = &*self.input_handles[i];
if p == handle {
let mut ih = self.input_handles.swap_remove(i);
let mut rv = false;
if let Err(e) = ih.scan_remainder() {
tt_warning!(self.status, "error closing out input {}", ih.name(); e);
rv = true;
}
let (name, digest_opt) = ih.into_name_digest();
self.hooks.event_input_closed(name, digest_opt, self.status);
return rv;
}
}
tt_error!(
self.status,
"serious internal bug: unexpected handle in input close: {:?}",
handle
);
true
}
fn shell_escape(&mut self, command: &str) -> bool {
if self.security.allow_shell_escape() {
match self.hooks.sysrq_shell_escape(command, self.status) {
Ok(_) => false,
Err(e) => {
tt_error!(
self.status,
"failed to execute the shell-escape command \"{}\": {}",
command,
e
);
true
}
}
} else {
tt_error!(
self.status,
"forbidden to execute shell-escape command \"{}\"",
command
);
true
}
}
}
#[derive(Clone, Debug)]
pub struct SecuritySettings {
disable_insecures: bool,
}
#[derive(Clone, Debug)]
pub enum SecurityStance {
DisableInsecures,
MaybeAllowInsecures,
}
impl Default for SecurityStance {
fn default() -> Self {
SecurityStance::DisableInsecures
}
}
impl SecuritySettings {
pub fn new(stance: SecurityStance) -> Self {
let disable_insecures = if std::env::var_os("TECTONIC_UNTRUSTED_MODE").is_some() {
true
} else {
match stance {
SecurityStance::DisableInsecures => true,
SecurityStance::MaybeAllowInsecures => false,
}
};
SecuritySettings { disable_insecures }
}
pub fn allow_shell_escape(&self) -> bool {
!self.disable_insecures
}
pub fn allow_extra_search_paths(&self) -> bool {
!self.disable_insecures
}
}
impl Default for SecuritySettings {
fn default() -> Self {
SecuritySettings::new(SecurityStance::default())
}
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_issue_warning(es: &mut CoreBridgeState, text: *const libc::c_char) {
let rtext = CStr::from_ptr(text);
tt_warning!(es.status, "{}", rtext.to_string_lossy());
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_issue_error(es: &mut CoreBridgeState, text: *const libc::c_char) {
let rtext = CStr::from_ptr(text);
tt_error!(es.status, "{}", rtext.to_string_lossy());
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_get_file_md5(
es: &mut CoreBridgeState,
path: *const libc::c_char,
digest: *mut u8,
) -> libc::c_int {
let rpath = CStr::from_ptr(path).to_string_lossy();
let rdest = slice::from_raw_parts_mut(digest, 16);
if es.get_file_md5(rpath.as_ref(), rdest) {
1
} else {
0
}
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_get_data_md5(
data: *const u8,
len: libc::size_t,
digest: *mut u8,
) -> libc::c_int {
let rdata = slice::from_raw_parts(data, len);
let rdest = slice::from_raw_parts_mut(digest, 16);
let mut hash = Md5::default();
hash.update(rdata);
let result = hash.finalize();
rdest.copy_from_slice(result.as_slice());
0
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_output_open(
es: &mut CoreBridgeState,
name: *const libc::c_char,
is_gz: libc::c_int,
) -> *mut OutputHandle {
let rname = CStr::from_ptr(name).to_string_lossy();
let ris_gz = is_gz != 0;
es.output_open(&rname, ris_gz)
}
#[no_mangle]
pub extern "C" fn ttbc_output_open_stdout(es: &mut CoreBridgeState) -> *mut OutputHandle {
es.output_open_stdout()
}
#[no_mangle]
pub extern "C" fn ttbc_output_putc(
es: &mut CoreBridgeState,
handle: *mut OutputHandle,
c: libc::c_int,
) -> libc::c_int {
let rc = c as u8;
if es.output_write(handle, &[rc]) {
libc::EOF
} else {
c
}
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_output_write(
es: &mut CoreBridgeState,
handle: *mut OutputHandle,
data: *const u8,
len: libc::size_t,
) -> libc::size_t {
let rdata = slice::from_raw_parts(data, len);
if es.output_write(handle, rdata) {
0
} else {
len
}
}
#[no_mangle]
pub extern "C" fn ttbc_output_flush(
es: &mut CoreBridgeState,
handle: *mut OutputHandle,
) -> libc::c_int {
if es.output_flush(handle) {
1
} else {
0
}
}
#[no_mangle]
pub extern "C" fn ttbc_output_close(
es: &mut CoreBridgeState,
handle: *mut OutputHandle,
) -> libc::c_int {
if handle.is_null() {
return 0; }
if es.output_close(handle) {
1
} else {
0
}
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_input_open(
es: &mut CoreBridgeState,
name: *const libc::c_char,
format: FileFormat,
is_gz: libc::c_int,
) -> *mut InputHandle {
let rname = CStr::from_ptr(name).to_string_lossy();
let ris_gz = is_gz != 0;
es.input_open(&rname, format, ris_gz)
}
#[no_mangle]
pub extern "C" fn ttbc_input_open_primary(es: &mut CoreBridgeState) -> *mut InputHandle {
es.input_open_primary()
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_get_last_input_abspath(
es: &mut CoreBridgeState,
buffer: *mut u8,
len: libc::size_t,
) -> libc::ssize_t {
match es.latest_input_path {
None => 0,
Some(ref p) => {
let p = match p.to_str() {
Some(s) => s.as_bytes(),
None => return -1,
};
let n = p.len();
if n + 1 > len {
return -2;
}
std::ptr::copy(p.as_ptr(), buffer, n);
*buffer.offset(n.try_into().unwrap()) = b'\0';
(n + 1).try_into().unwrap()
}
}
}
#[no_mangle]
pub extern "C" fn ttbc_input_get_size(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
) -> libc::size_t {
es.input_get_size(handle)
}
#[no_mangle]
pub extern "C" fn ttbc_input_get_mtime(es: &mut CoreBridgeState, handle: *mut InputHandle) -> i64 {
es.input_get_mtime(handle)
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_input_seek(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
offset: libc::ssize_t,
whence: libc::c_int,
internal_error: *mut libc::c_int,
) -> libc::size_t {
let rwhence = match whence {
libc::SEEK_SET => SeekFrom::Start(offset as u64),
libc::SEEK_CUR => SeekFrom::Current(offset as i64),
libc::SEEK_END => SeekFrom::End(offset as i64),
_ => {
tt_error!(
es.status,
"serious internal bug: unexpected \"whence\" parameter to fseek() wrapper: {}",
whence
);
*internal_error = 1;
return 0;
}
};
match es.input_seek(handle, rwhence) {
Ok(pos) => pos as libc::size_t,
Err(e) => {
tt_error!(es.status, "input seek failed"; e);
0
}
}
}
#[no_mangle]
pub extern "C" fn ttbc_input_getc(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
) -> libc::c_int {
match es.input_getc(handle) {
Ok(b) => libc::c_int::from(b),
Err(e) => {
if let Some(ioe) = e.downcast_ref::<io::Error>() {
if ioe.kind() == io::ErrorKind::UnexpectedEof {
return libc::EOF;
}
}
tt_warning!(es.status, "getc failed"; e);
-1
}
}
}
#[no_mangle]
pub extern "C" fn ttbc_input_ungetc(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
ch: libc::c_int,
) -> libc::c_int {
match es.input_ungetc(handle, ch as u8) {
Ok(_) => 0,
Err(e) => {
tt_warning!(es.status, "ungetc() failed"; e);
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_input_read(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
data: *mut u8,
len: libc::size_t,
) -> libc::ssize_t {
let rdata = slice::from_raw_parts_mut(data, len);
match es.input_read(handle, rdata) {
Ok(_) => len as isize,
Err(e) => {
tt_warning!(es.status, "{}-byte read failed", len; e);
-1
}
}
}
#[no_mangle]
pub extern "C" fn ttbc_input_close(
es: &mut CoreBridgeState,
handle: *mut InputHandle,
) -> libc::c_int {
if handle.is_null() {
return 0; }
if es.input_close(handle) {
1
} else {
0
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
message: String,
kind: MessageKind,
}
#[no_mangle]
pub extern "C" fn ttbc_diag_begin_warning() -> *mut Diagnostic {
let warning = Box::new(Diagnostic {
message: String::new(),
kind: MessageKind::Warning,
});
Box::into_raw(warning)
}
#[no_mangle]
pub extern "C" fn ttbc_diag_begin_error() -> *mut Diagnostic {
let warning = Box::new(Diagnostic {
message: String::new(),
kind: MessageKind::Error,
});
Box::into_raw(warning)
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_diag_append(diag: &mut Diagnostic, text: *const libc::c_char) {
let rtext = CStr::from_ptr(text);
diag.message.push_str(&rtext.to_string_lossy());
}
#[no_mangle]
pub extern "C" fn ttbc_diag_finish(es: &mut CoreBridgeState, diag: *mut Diagnostic) {
let rdiag = unsafe { Box::from_raw(diag as *mut Diagnostic) };
es.status
.report(rdiag.kind, format_args!("{}", rdiag.message), None);
}
#[no_mangle]
pub unsafe extern "C" fn ttbc_shell_escape(
es: &mut CoreBridgeState,
cmd: *const u16,
len: libc::size_t,
) -> libc::c_int {
let rcmd = slice::from_raw_parts(cmd, len);
let rcmd = match String::from_utf16(rcmd) {
Ok(cmd) => cmd,
Err(err) => {
tt_error!(es.status, "command was not valid UTF-16"; err.into());
return -1;
}
};
if es.shell_escape(&rcmd) {
1
} else {
0
}
}
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub enum FileFormat {
Afm = 4,
Bib = 6,
Bst = 7,
Cmap = 45,
Cnf = 8,
Enc = 44,
Format = 10,
FontMap = 11,
MiscFonts = 41,
Ofm = 20,
OpenType = 47,
Ovf = 23,
Pict = 25,
Pk = 1,
ProgramData = 39,
Sfd = 46,
TectonicPrimary = 59,
Tex = 26,
TexPsHeader = 30,
Tfm = 3,
TrueType = 36,
Type1 = 32,
Vf = 33,
}
impl FileFormat {
fn extensions(&self) -> &[&str] {
match *self {
FileFormat::Afm => &["afm"],
FileFormat::Bib => &["bib"],
FileFormat::Bst => &["bst"],
FileFormat::Cmap => &[],
FileFormat::Cnf => &["cnf"],
FileFormat::Enc => &["enc"],
FileFormat::Format => &["fmt"],
FileFormat::FontMap => &["map"],
FileFormat::MiscFonts => &[],
FileFormat::Ofm => &["ofm"],
FileFormat::OpenType => &["otf", "OTF"],
FileFormat::Ovf => &["ovf", "vf"],
FileFormat::Pict => &["pdf", "jpg", "eps", "epsi"],
FileFormat::Pk => &["pk"],
FileFormat::ProgramData => &[],
FileFormat::Sfd => &["sfd"],
FileFormat::TectonicPrimary => &[],
FileFormat::Tex => &["tex", "sty", "cls", "fd", "aux", "bbl", "def", "clo", "ldf"],
FileFormat::TexPsHeader => &["pro"],
FileFormat::Tfm => &["tfm"],
FileFormat::TrueType => &["ttf", "ttc", "TTF", "TTC", "dfont"],
FileFormat::Type1 => &["pfa", "pfb"],
FileFormat::Vf => &["vf"],
}
}
}
#[test]
fn linkage() {}