oxidized_importer/
importer.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*!
6Functionality for a Python importer.
7
8This module defines a Python meta path importer and associated functionality
9for importing Python modules from memory.
10*/
11
12#[cfg(windows)]
13use {
14    crate::memory_dll::{free_library_memory, get_proc_address_memory, load_library_memory},
15    pyo3::exceptions::PySystemError,
16    std::ffi::{c_void, CString},
17};
18use {
19    crate::{
20        conversion::pyobject_to_pathbuf,
21        get_module_state,
22        path_entry_finder::OxidizedPathEntryFinder,
23        pkg_resources::register_pkg_resources_with_module,
24        python_resources::{
25            pyobject_to_resource, ModuleFlavor, OxidizedResource, PythonResourcesState,
26        },
27        resource_reader::OxidizedResourceReader,
28        OXIDIZED_IMPORTER_NAME_STR,
29    },
30    pyo3::{
31        exceptions::{PyImportError, PyValueError},
32        ffi as pyffi,
33        prelude::*,
34        types::{PyBytes, PyDict, PyList, PyString, PyTuple},
35        AsPyPointer, FromPyPointer, PyNativeType, PyTraverseError, PyVisit,
36    },
37    python_packaging::resource::BytecodeOptimizationLevel,
38    std::sync::Arc,
39};
40
41#[cfg(windows)]
42#[allow(non_camel_case_types)]
43type py_init_fn = extern "C" fn() -> *mut pyffi::PyObject;
44
45/// Implementation of `Loader.create_module()` for in-memory extension modules.
46///
47/// The equivalent CPython code for importing extension modules is to call
48/// `imp.create_dynamic()`. This will:
49///
50/// 1. Call `_PyImport_FindExtensionObject()`.
51/// 2. Call `_PyImport_LoadDynamicModuleWithSpec()` if #1 didn't return anything.
52///
53/// While `_PyImport_FindExtensionObject()` accepts a `filename` argument, this
54/// argument is only used as a key inside an internal dictionary indexing found
55/// extension modules. So we can call that function verbatim.
56///
57/// `_PyImport_LoadDynamicModuleWithSpec()` is more interesting. It takes a
58/// `FILE*` for the extension location, so we can't call it. So we need to
59/// reimplement it. Documentation of that is inline.
60#[cfg(windows)]
61fn extension_module_shared_library_create_module(
62    resources_state: &PythonResourcesState<u8>,
63    py: Python,
64    sys_modules: &PyAny,
65    spec: &PyAny,
66    name_py: &PyAny,
67    name: &str,
68    library_data: &[u8],
69) -> PyResult<Py<PyAny>> {
70    let origin = PyString::new(py, "memory");
71
72    let existing_module =
73        unsafe { pyffi::_PyImport_FindExtensionObject(name_py.as_ptr(), origin.as_ptr()) };
74
75    // We found an existing module object. Return it.
76    if !existing_module.is_null() {
77        return Ok(unsafe { PyObject::from_owned_ptr(py, existing_module) });
78    }
79
80    // An error occurred calling _PyImport_FindExtensionObjectEx(). Raise it.
81    if !unsafe { pyffi::PyErr_Occurred() }.is_null() {
82        return Err(PyErr::fetch(py));
83    }
84
85    // New module load request. Proceed to _PyImport_LoadDynamicModuleWithSpec()
86    // functionality.
87
88    let module = unsafe { load_library_memory(resources_state, library_data) };
89
90    if module.is_null() {
91        return Err(PyImportError::new_err((
92            "unable to load extension module library from memory",
93            name.to_owned(),
94        )));
95    }
96
97    // Any error past this point should call `MemoryFreeLibrary()` to unload the
98    // library.
99
100    load_dynamic_library(py, sys_modules, spec, name_py, name, module).map_err(|e| {
101        unsafe {
102            free_library_memory(module);
103        }
104        e
105    })
106}
107
108#[cfg(unix)]
109fn extension_module_shared_library_create_module(
110    _resources_state: &PythonResourcesState<u8>,
111    _py: Python,
112    _sys_modules: &PyAny,
113    _spec: &PyAny,
114    _name_py: &PyAny,
115    _name: &str,
116    _library_data: &[u8],
117) -> PyResult<Py<PyAny>> {
118    panic!("should only be called on Windows");
119}
120
121/// Reimplementation of `_PyImport_LoadDynamicModuleWithSpec()`.
122#[cfg(windows)]
123fn load_dynamic_library(
124    py: Python,
125    sys_modules: &PyAny,
126    spec: &PyAny,
127    name_py: &PyAny,
128    name: &str,
129    library_module: *const c_void,
130) -> PyResult<Py<PyAny>> {
131    // The init function is `PyInit_<stem>`.
132    let last_name_part = if name.contains('.') {
133        name.split('.').last().unwrap()
134    } else {
135        name
136    };
137
138    let name_cstring = CString::new(name).unwrap();
139    let init_fn_name = CString::new(format!("PyInit_{}", last_name_part)).unwrap();
140
141    let address = unsafe { get_proc_address_memory(library_module, &init_fn_name) };
142    if address.is_null() {
143        return Err(PyImportError::new_err((
144            format!(
145                "dynamic module does not define module export function ({})",
146                init_fn_name.to_str().unwrap()
147            ),
148            name.to_owned(),
149        )));
150    }
151
152    let init_fn: py_init_fn = unsafe { std::mem::transmute(address) };
153
154    // Package context is needed for single-phase init.
155    let py_module = unsafe {
156        let old_context = pyffi::_Py_PackageContext;
157        pyffi::_Py_PackageContext = name_cstring.as_ptr();
158        let py_module = init_fn();
159        pyffi::_Py_PackageContext = old_context;
160        py_module
161    };
162
163    // The initialization function will return a new/owned reference for single-phase initialization
164    // and a borrowed reference for multi-phase initialization. Since we don't know which form
165    // we're using until later, we need to be very careful about handling py_module here! Note
166    // that it may be possible to leak an owned reference in the error handling below. This
167    // code mimics what CPython does and the leak, if any, likely occurs there as well.
168
169    if py_module.is_null() && unsafe { pyffi::PyErr_Occurred().is_null() } {
170        return Err(PySystemError::new_err(format!(
171            "initialization of {} failed without raising an exception",
172            name
173        )));
174    }
175
176    if !unsafe { pyffi::PyErr_Occurred().is_null() } {
177        unsafe {
178            pyffi::PyErr_Clear();
179        }
180        return Err(PySystemError::new_err(format!(
181            "initialization of {} raised unreported exception",
182            name
183        )));
184    }
185
186    if unsafe { pyffi::Py_TYPE(py_module) }.is_null() {
187        return Err(PySystemError::new_err(format!(
188            "init function of {} returned uninitialized object",
189            name
190        )));
191    }
192
193    // If initialization returned a `PyModuleDef`, this is multi-phase initialization. Construct a
194    // module by calling PyModule_FromDefAndSpec(). py_module is a borrowed reference. And
195    // PyModule_FromDefAndSpec() returns a new reference. So we don't need to worry about refcounts
196    // of py_module.
197    if unsafe { pyffi::PyObject_TypeCheck(py_module, &mut pyffi::PyModuleDef_Type) } != 0 {
198        let py_module = unsafe {
199            pyffi::PyModule_FromDefAndSpec(py_module as *mut pyffi::PyModuleDef, spec.as_ptr())
200        };
201
202        return if py_module.is_null() {
203            Err(PyErr::fetch(py))
204        } else {
205            Ok(unsafe { PyObject::from_owned_ptr(py, py_module) })
206        };
207    }
208
209    // This is the single-phase initialization mechanism. Construct a module by calling
210    // PyModule_GetDef(). py_module is a new reference. So we capture it to make sure we don't
211    // leak it.
212    let py_module = unsafe { PyObject::from_owned_ptr(py, py_module) };
213
214    let mut module_def = unsafe { pyffi::PyModule_GetDef(py_module.as_ptr()) };
215    if module_def.is_null() {
216        return Err(PySystemError::new_err(format!(
217            "initialization of {} did not return an extension module",
218            name
219        )));
220    }
221
222    unsafe {
223        (*module_def).m_base.m_init = Some(init_fn);
224    }
225
226    // If we wanted to assign __file__ we would do it here.
227
228    let fixup_result = unsafe {
229        pyffi::_PyImport_FixupExtensionObject(
230            py_module.as_ptr(),
231            name_py.as_ptr(),
232            name_py.as_ptr(),
233            sys_modules.as_ptr(),
234        )
235    };
236
237    if fixup_result < 0 {
238        Err(PyErr::fetch(py))
239    } else {
240        Ok(py_module)
241    }
242}
243
244/// Holds state for the custom MetaPathFinder.
245pub struct ImporterState {
246    /// `imp` Python module.
247    pub(crate) imp_module: Py<PyModule>,
248    /// `sys` Python module.
249    pub(crate) sys_module: Py<PyModule>,
250    /// `_io` Python module.
251    pub(crate) io_module: Py<PyModule>,
252    /// `marshal.loads` Python callable.
253    pub(crate) marshal_loads: Py<PyAny>,
254    /// `_frozen_importlib.BuiltinImporter` meta path importer for built-in extension modules.
255    pub(crate) builtin_importer: Py<PyAny>,
256    /// `_frozen_importlib.FrozenImporter` meta path importer for frozen modules.
257    pub(crate) frozen_importer: Py<PyAny>,
258    /// `importlib._bootstrap._call_with_frames_removed` function.
259    pub(crate) call_with_frames_removed: Py<PyAny>,
260    /// `importlib._bootstrap.ModuleSpec` class.
261    pub(crate) module_spec_type: Py<PyAny>,
262    /// Our `decode_source()` function.
263    pub(crate) decode_source: Py<PyAny>,
264    /// `builtins.exec` function.
265    pub(crate) exec_fn: Py<PyAny>,
266    /// Bytecode optimization level currently in effect.
267    pub(crate) optimize_level: BytecodeOptimizationLevel,
268    /// Value to pass to `multiprocessing.set_start_method()` on import of `multiprocessing`.
269    ///
270    /// If `None`, `set_start_method()` will not be called automatically.
271    pub(crate) multiprocessing_set_start_method: Option<String>,
272    /// Whether to automatically register ourself with `pkg_resources` when it is imported.
273    pub(crate) pkg_resources_import_auto_register: bool,
274    /// Holds state about importable resources.
275    ///
276    /// This field is a PyCapsule and is a glorified wrapper around
277    /// a pointer. That pointer refers to heap backed memory.
278    ///
279    /// The memory behind the pointer can either by owned by us or owned
280    /// externally. If owned externally, the memory is likely backed by
281    /// the `MainPythonInterpreter` instance that spawned us.
282    ///
283    /// Storing a pointer this way avoids Rust lifetime checks and allows
284    /// us to side-step the requirement that all lifetimes in Python
285    /// objects be 'static. This allows us to use proper lifetimes for
286    /// the backing memory instead of forcing all resource data to be backed
287    /// by 'static.
288    pub(crate) resources_state: Py<PyAny>,
289}
290
291impl ImporterState {
292    fn new<'a>(
293        py: Python,
294        importer_module: &PyModule,
295        bootstrap_module: &PyModule,
296        resources_state: Box<PythonResourcesState<'a, u8>>,
297    ) -> Result<Self, PyErr> {
298        let decode_source = importer_module.getattr("decode_source")?.into_py(py);
299
300        let io_module = py.import("_io")?.into_py(py);
301        let marshal_module = py.import("marshal")?;
302
303        let imp_module = bootstrap_module.getattr("_imp")?;
304        let imp_module = imp_module.cast_as::<PyModule>()?.into_py(py);
305        let sys_module = bootstrap_module.getattr("sys")?;
306        let sys_module = sys_module.cast_as::<PyModule>()?;
307        let meta_path_object = sys_module.getattr("meta_path")?;
308
309        // We should be executing as part of
310        // _frozen_importlib_external._install_external_importers().
311        // _frozen_importlib._install() should have already been called and set up
312        // sys.meta_path with [BuiltinImporter, FrozenImporter]. Those should be the
313        // only meta path importers present.
314
315        let meta_path = meta_path_object.cast_as::<PyList>()?;
316        if meta_path.len() < 2 {
317            return Err(PyValueError::new_err(
318                "sys.meta_path does not contain 2 values",
319            ));
320        }
321
322        let builtin_importer = meta_path.get_item(0)?.into_py(py);
323        let frozen_importer = meta_path.get_item(1)?.into_py(py);
324
325        let marshal_loads = marshal_module.getattr("loads")?.into_py(py);
326        let call_with_frames_removed = bootstrap_module
327            .getattr("_call_with_frames_removed")?
328            .into_py(py);
329        let module_spec_type = bootstrap_module.getattr("ModuleSpec")?.into_py(py);
330
331        let builtins_module =
332            unsafe { PyDict::from_borrowed_ptr_or_err(py, pyffi::PyEval_GetBuiltins()) }?;
333
334        let exec_fn = match builtins_module.get_item("exec") {
335            Some(v) => v,
336            None => {
337                return Err(PyValueError::new_err("could not obtain __builtins__.exec"));
338            }
339        }
340        .into_py(py);
341
342        let sys_flags = sys_module.getattr("flags")?;
343        let sys_module = sys_module.into_py(py);
344
345        let optimize_value = sys_flags.getattr("optimize")?;
346        let optimize_value = optimize_value.extract::<i64>()?;
347
348        let optimize_level = match optimize_value {
349            0 => Ok(BytecodeOptimizationLevel::Zero),
350            1 => Ok(BytecodeOptimizationLevel::One),
351            2 => Ok(BytecodeOptimizationLevel::Two),
352            _ => Err(PyValueError::new_err(
353                "unexpected value for sys.flags.optimize",
354            )),
355        }?;
356
357        let capsule = unsafe {
358            let ptr = pyffi::PyCapsule_New(
359                &*resources_state as *const PythonResourcesState<u8> as *mut _,
360                std::ptr::null(),
361                None,
362            );
363
364            if ptr.is_null() {
365                return Err(PyValueError::new_err(
366                    "unable to convert PythonResourcesState to capsule",
367                ));
368            }
369
370            PyObject::from_owned_ptr(py, ptr)
371        };
372
373        // We store a pointer to the heap memory and take care of destroying
374        // it when we are dropped. So we leak the box.
375        Box::leak(resources_state);
376
377        Ok(ImporterState {
378            imp_module,
379            sys_module,
380            io_module,
381            marshal_loads,
382            builtin_importer,
383            frozen_importer,
384            call_with_frames_removed,
385            module_spec_type,
386            decode_source,
387            exec_fn,
388            optimize_level,
389            multiprocessing_set_start_method: None,
390            // TODO value should come from config.
391            pkg_resources_import_auto_register: true,
392            resources_state: capsule,
393        })
394    }
395
396    /// Perform garbage collection traversal on this instance.
397    ///
398    /// Do NOT make this pub(crate) because in most cases holders do not need to traverse
399    /// into this since they have an `Arc<T>` reference, not a `Py<T>` reference. Only the
400    /// canonical holder of this instance should call Python's gc visiting.
401    fn gc_traverse(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
402        visit.call(&self.imp_module)?;
403        visit.call(&self.sys_module)?;
404        visit.call(&self.io_module)?;
405        visit.call(&self.marshal_loads)?;
406        visit.call(&self.builtin_importer)?;
407        visit.call(&self.frozen_importer)?;
408        visit.call(&self.call_with_frames_removed)?;
409        visit.call(&self.module_spec_type)?;
410        visit.call(&self.decode_source)?;
411        visit.call(&self.exec_fn)?;
412        visit.call(&self.resources_state)?;
413
414        Ok(())
415    }
416
417    /// Obtain the `PythonResourcesState` associated with this instance.
418    #[inline]
419    pub fn get_resources_state<'a>(&self) -> &PythonResourcesState<'a, u8> {
420        let ptr =
421            unsafe { pyffi::PyCapsule_GetPointer(self.resources_state.as_ptr(), std::ptr::null()) };
422
423        if ptr.is_null() {
424            panic!("null pointer in resources state capsule");
425        }
426
427        unsafe { &*(ptr as *const PythonResourcesState<u8>) }
428    }
429
430    /// Obtain a mutable `PythonResourcesState` associated with this instance.
431    ///
432    /// There is no run-time checking for mutation exclusion. So don't like this
433    /// leak outside of a single call site that needs to access it!
434    #[allow(clippy::mut_from_ref)]
435    pub fn get_resources_state_mut<'a>(&self) -> &mut PythonResourcesState<'a, u8> {
436        let ptr =
437            unsafe { pyffi::PyCapsule_GetPointer(self.resources_state.as_ptr(), std::ptr::null()) };
438
439        if ptr.is_null() {
440            panic!("null pointer in resources state capsule");
441        }
442
443        unsafe { &mut *(ptr as *mut PythonResourcesState<u8>) }
444    }
445
446    /// Set the value to call `multiprocessing.set_start_method()` with on import of `multiprocessing`.
447    #[allow(unused)]
448    pub fn set_multiprocessing_set_start_method(&mut self, value: Option<String>) {
449        self.multiprocessing_set_start_method = value;
450    }
451}
452
453impl Drop for ImporterState {
454    fn drop(&mut self) {
455        let ptr =
456            unsafe { pyffi::PyCapsule_GetPointer(self.resources_state.as_ptr(), std::ptr::null()) };
457
458        if !ptr.is_null() {
459            unsafe {
460                drop(Box::from_raw(ptr as *mut PythonResourcesState<u8>));
461            }
462        }
463    }
464}
465
466/// Python type to import modules.
467///
468/// This type implements the importlib.abc.MetaPathFinder interface for
469/// finding/loading modules. It supports loading various flavors of modules,
470/// allowing it to be the only registered sys.meta_path importer.
471#[pyclass(module = "oxidized_importer")]
472pub struct OxidizedFinder {
473    pub(crate) state: Arc<ImporterState>,
474}
475
476impl OxidizedFinder {
477    pub(crate) fn get_state(&self) -> Arc<ImporterState> {
478        self.state.clone()
479    }
480
481    /// Construct an instance from a module and resources state.
482    pub fn new_from_module_and_resources<'a>(
483        py: Python,
484        m: &PyModule,
485        resources_state: Box<PythonResourcesState<'a, u8>>,
486        importer_state_callback: Option<impl FnOnce(&mut ImporterState)>,
487    ) -> PyResult<OxidizedFinder> {
488        let bootstrap_module = py.import("_frozen_importlib")?;
489
490        let mut importer_state = Arc::new(ImporterState::new(
491            py,
492            m,
493            bootstrap_module,
494            resources_state,
495        )?);
496
497        if let Some(cb) = importer_state_callback {
498            let state_ref = Arc::<ImporterState>::get_mut(&mut importer_state)
499                .expect("Arc::get_mut() should work");
500            cb(state_ref);
501        }
502
503        Ok(OxidizedFinder {
504            state: importer_state,
505        })
506    }
507}
508
509#[pymethods]
510impl OxidizedFinder {
511    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
512        self.state.gc_traverse(visit)
513    }
514
515    // Start of importlib.abc.MetaPathFinder interface.
516
517    #[args(target = "None")]
518    fn find_spec<'p>(
519        slf: &'p PyCell<Self>,
520        fullname: String,
521        path: &PyAny,
522        target: Option<&PyAny>,
523    ) -> PyResult<&'p PyAny> {
524        let py = slf.py();
525        let finder = slf.borrow();
526
527        let module = match finder
528            .state
529            .get_resources_state()
530            .resolve_importable_module(&fullname, finder.state.optimize_level)
531        {
532            Some(module) => module,
533            None => return Ok(py.None().into_ref(py)),
534        };
535
536        match module.flavor {
537            ModuleFlavor::Extension | ModuleFlavor::SourceBytecode => module.resolve_module_spec(
538                py,
539                finder.state.module_spec_type.clone_ref(py).into_ref(py),
540                slf,
541                finder.state.optimize_level,
542            ),
543            ModuleFlavor::Builtin => {
544                // BuiltinImporter.find_spec() always returns None if `path` is defined.
545                // And it doesn't use `target`. So don't proxy these values.
546                Ok(finder
547                    .state
548                    .builtin_importer
549                    .call_method(py, "find_spec", (fullname,), None)?
550                    .into_ref(py))
551            }
552            ModuleFlavor::Frozen => Ok(finder
553                .state
554                .frozen_importer
555                .call_method(py, "find_spec", (fullname, path, target), None)?
556                .into_ref(py)),
557        }
558    }
559
560    fn find_module<'p>(
561        slf: &'p PyCell<Self>,
562        fullname: &PyAny,
563        path: &PyAny,
564    ) -> PyResult<&'p PyAny> {
565        let find_spec = slf.getattr("find_spec")?;
566        let spec = find_spec.call((fullname, path), None)?;
567
568        if spec.is_none() {
569            Ok(slf.py().None().into_ref(slf.py()))
570        } else {
571            spec.getattr("loader")
572        }
573    }
574
575    fn invalidate_caches(&self) -> PyResult<()> {
576        Ok(())
577    }
578
579    // End of importlib.abc.MetaPathFinder interface.
580
581    // Start of importlib.abc.Loader interface.
582
583    fn create_module(slf: &PyCell<Self>, spec: &PyAny) -> PyResult<Py<PyAny>> {
584        let py = slf.py();
585        let finder = slf.borrow();
586        let state = &finder.state;
587
588        let name = spec.getattr("name")?;
589        let key = name.extract::<String>()?;
590
591        let module = match state
592            .get_resources_state()
593            .resolve_importable_module(&key, state.optimize_level)
594        {
595            Some(module) => module,
596            None => return Ok(py.None()),
597        };
598
599        // Extension modules need special module creation logic.
600        if module.flavor == ModuleFlavor::Extension {
601            // We need a custom implementation of create_module() for in-memory shared
602            // library extensions because if we wait until `exec_module()` to
603            // initialize the module object, this can confuse some CPython
604            // internals. A side-effect of initializing extension modules is
605            // populating `sys.modules` and this made `LazyLoader` unhappy.
606            // If we ever implement our own lazy module importer, we could
607            // potentially work around this and move all extension module
608            // initialization into `exec_module()`.
609            if let Some(library_data) = &module.in_memory_extension_module_shared_library() {
610                let sys_modules = state.sys_module.getattr(py, "modules")?;
611
612                extension_module_shared_library_create_module(
613                    state.get_resources_state(),
614                    py,
615                    sys_modules.into_ref(py),
616                    spec,
617                    name,
618                    &key,
619                    library_data,
620                )
621            } else {
622                // Call `imp.create_dynamic()` for dynamic extension modules.
623                let create_dynamic = state.imp_module.getattr(py, "create_dynamic")?;
624
625                state
626                    .call_with_frames_removed
627                    .call(py, (&create_dynamic, spec), None)
628            }
629        } else {
630            Ok(py.None())
631        }
632    }
633
634    fn exec_module(slf: &PyCell<Self>, module: &PyAny) -> PyResult<Py<PyAny>> {
635        let py = slf.py();
636        let finder = slf.borrow();
637        let state = &finder.state;
638
639        let name = module.getattr("__name__")?;
640        let key = name.extract::<String>()?;
641
642        let mut entry = match state
643            .get_resources_state()
644            .resolve_importable_module(&key, state.optimize_level)
645        {
646            Some(entry) => entry,
647            None => {
648                // Raising here might make more sense, as `find_spec()` shouldn't have returned
649                // an entry for something that we don't know how to handle.
650                return Ok(py.None());
651            }
652        };
653
654        if let Some(bytecode) = entry.resolve_bytecode(
655            py,
656            state.optimize_level,
657            state.decode_source.as_ref(py),
658            state.io_module.as_ref(py),
659        )? {
660            let code = state.marshal_loads.call(py, (bytecode,), None)?;
661            let dict = module.getattr("__dict__")?;
662
663            state
664                .call_with_frames_removed
665                .call(py, (&state.exec_fn, code, dict), None)
666        } else if entry.flavor == ModuleFlavor::Builtin {
667            state
668                .builtin_importer
669                .call_method(py, "exec_module", (module,), None)
670        } else if entry.flavor == ModuleFlavor::Frozen {
671            state
672                .frozen_importer
673                .call_method(py, "exec_module", (module,), None)
674        } else if entry.flavor == ModuleFlavor::Extension {
675            // `ExtensionFileLoader.exec_module()` simply calls `imp.exec_dynamic()`.
676            let exec_dynamic = state.imp_module.getattr(py, "exec_dynamic")?;
677
678            state
679                .call_with_frames_removed
680                .call(py, (&exec_dynamic, module), None)
681        } else {
682            Ok(py.None())
683        }?;
684
685        // Perform import time side-effects for special modules.
686        match key.as_str() {
687            "multiprocessing" => {
688                if let Some(method) = state.multiprocessing_set_start_method.as_ref() {
689                    // We pass force=True to ensure the call doesn't fail.
690                    let kwargs = PyDict::new(py);
691                    kwargs.set_item("force", true)?;
692                    module.call_method("set_start_method", (method,), Some(kwargs))?;
693                }
694            }
695            "pkg_resources" => {
696                if state.pkg_resources_import_auto_register {
697                    register_pkg_resources_with_module(py, module)?;
698                }
699            }
700            _ => {}
701        }
702
703        Ok(py.None())
704    }
705
706    // End of importlib.abc.Loader interface.
707
708    // Start of importlib.abc.ResourceLoader interface.
709
710    /// An abstract method to return the bytes for the data located at path.
711    ///
712    /// Loaders that have a file-like storage back-end that allows storing
713    /// arbitrary data can implement this abstract method to give direct access
714    /// to the data stored. OSError is to be raised if the path cannot be
715    /// found. The path is expected to be constructed using a module’s __file__
716    /// attribute or an item from a package’s __path__.
717    fn get_data<'p>(slf: &'p PyCell<Self>, path: &str) -> PyResult<&'p PyAny> {
718        slf.borrow()
719            .state
720            .get_resources_state()
721            .resolve_resource_data_from_path(slf.py(), path)
722    }
723
724    // End of importlib.abs.ResourceLoader interface.
725
726    // Start of importlib.abc.InspectLoader interface.
727
728    fn get_code(slf: &PyCell<Self>, fullname: &str) -> PyResult<Py<PyAny>> {
729        let py = slf.py();
730        let finder = slf.borrow();
731        let state = &finder.state;
732
733        let key = fullname.to_string();
734
735        let mut module = match state
736            .get_resources_state()
737            .resolve_importable_module(&key, state.optimize_level)
738        {
739            Some(module) => module,
740            None => return Ok(py.None()),
741        };
742
743        if let Some(bytecode) = module.resolve_bytecode(
744            py,
745            state.optimize_level,
746            state.decode_source.as_ref(py),
747            state.io_module.as_ref(py),
748        )? {
749            state.marshal_loads.call(py, (bytecode,), None)
750        } else if module.flavor == ModuleFlavor::Frozen {
751            state
752                .imp_module
753                .call_method(py, "get_frozen_object", (fullname,), None)
754        } else {
755            Ok(py.None())
756        }
757    }
758
759    fn get_source(slf: &PyCell<Self>, fullname: &str) -> PyResult<Py<PyAny>> {
760        let py = slf.py();
761        let finder = slf.borrow();
762        let state = &finder.state;
763        let key = fullname.to_string();
764
765        let module = match state
766            .get_resources_state()
767            .resolve_importable_module(&key, state.optimize_level)
768        {
769            Some(module) => module,
770            None => return Ok(py.None()),
771        };
772
773        let source = module.resolve_source(
774            py,
775            state.decode_source.as_ref(py),
776            state.io_module.as_ref(py),
777        )?;
778
779        Ok(if let Some(source) = source {
780            source.into_py(py)
781        } else {
782            py.None()
783        })
784    }
785
786    // Start of importlib.abc.ExecutionLoader interface.
787
788    /// An abstract method that is to return the value of __file__ for the specified module.
789    ///
790    /// If no path is available, ImportError is raised.
791    ///
792    /// If source code is available, then the method should return the path to the
793    /// source file, regardless of whether a bytecode was used to load the module.
794    fn get_filename<'p>(slf: &'p PyCell<Self>, fullname: &str) -> PyResult<&'p PyAny> {
795        let finder = slf.borrow();
796        let state = &finder.state;
797        let key = fullname.to_string();
798
799        let make_error =
800            |msg: &str| -> PyErr { PyImportError::new_err((msg.to_owned(), key.clone())) };
801
802        let module = state
803            .get_resources_state()
804            .resolve_importable_module(&key, state.optimize_level)
805            .ok_or_else(|| make_error("unknown module"))?;
806
807        module
808            .resolve_origin(slf.py())
809            .map_err(|_| make_error("unable to resolve origin"))?
810            .ok_or_else(|| make_error("no origin"))
811    }
812
813    // End of importlib.abc.ExecutionLoader interface.
814
815    // End of importlib.abc.InspectLoader interface.
816
817    // Support obtaining ResourceReader instances.
818
819    fn get_resource_reader(slf: &PyCell<Self>, fullname: &str) -> PyResult<Py<PyAny>> {
820        let finder = slf.borrow();
821        let state = &finder.state;
822        let key = fullname.to_string();
823
824        let entry = match state
825            .get_resources_state()
826            .resolve_importable_module(&key, state.optimize_level)
827        {
828            Some(entry) => entry,
829            None => return Ok(slf.py().None()),
830        };
831
832        // Resources are only available on packages.
833        if entry.is_package {
834            Ok(PyCell::new(
835                slf.py(),
836                OxidizedResourceReader::new(state.clone(), key.to_string()),
837            )?
838            .into_py(slf.py()))
839        } else {
840            Ok(slf.py().None())
841        }
842    }
843
844    // importlib.metadata interface.
845
846    /// def find_distributions(context=DistributionFinder.Context()):
847    ///
848    /// Return an iterable of all Distribution instances capable of
849    /// loading the metadata for packages for the indicated `context`.
850    ///
851    /// The DistributionFinder.Context object provides .path and .name
852    /// properties indicating the path to search and names to match and
853    /// may supply other relevant context.
854    ///
855    /// What this means in practice is that to support finding distribution
856    /// package metadata in locations other than the file system, subclass
857    /// Distribution and implement the abstract methods. Then from a custom
858    /// finder, return instances of this derived Distribution in the
859    /// find_distributions() method.
860    #[args(context = "None")]
861    fn find_distributions<'p>(
862        slf: &'p PyCell<Self>,
863        context: Option<&PyAny>,
864    ) -> PyResult<&'p PyAny> {
865        let py = slf.py();
866        let finder = slf.borrow();
867        let state = &finder.state;
868
869        let (path, name) = if let Some(context) = context {
870            // The passed object should have `path` and `name` attributes. But the
871            // values could be `None`, so normalize those to Rust's `None`.
872            let path = context.getattr("path")?;
873            let path = if path.is_none() { None } else { Some(path) };
874
875            let name = context.getattr("name")?;
876            let name = if name.is_none() { None } else { Some(name) };
877
878            (path, name)
879        } else {
880            // No argument = default Context = find everything.
881            (None, None)
882        };
883
884        crate::package_metadata::find_distributions(py, state.clone(), name, path)?
885            .call_method0("__iter__")
886    }
887
888    // pkgutil methods.
889
890    /// def iter_modules(prefix="")
891    #[args(prefix = "None")]
892    fn iter_modules<'p>(slf: &'p PyCell<Self>, prefix: Option<&str>) -> PyResult<&'p PyList> {
893        let finder = slf.borrow();
894        let state = &finder.state;
895
896        let resources_state = state.get_resources_state();
897
898        let prefix = prefix.map(|prefix| prefix.to_string());
899
900        resources_state.pkgutil_modules_infos(slf.py(), None, prefix, state.optimize_level)
901    }
902
903    // Additional methods provided for convenience.
904
905    /// OxidizedFinder.__new__(relative_path_origin=None))
906    #[new]
907    #[args(relative_path_origin = "None")]
908    fn new(py: Python, relative_path_origin: Option<&PyAny>) -> PyResult<Self> {
909        // We need to obtain an ImporterState instance. This requires handles on a
910        // few items...
911
912        // The module references are easy to obtain.
913        let m = py.import(OXIDIZED_IMPORTER_NAME_STR)?;
914        let bootstrap_module = py.import("_frozen_importlib")?;
915
916        let mut resources_state =
917            Box::new(PythonResourcesState::new_from_env().map_err(PyValueError::new_err)?);
918
919        // Update origin if a value is given.
920        if let Some(py_origin) = relative_path_origin {
921            resources_state.set_origin(pyobject_to_pathbuf(py, py_origin)?);
922        }
923
924        Ok(OxidizedFinder {
925            state: Arc::new(ImporterState::new(
926                py,
927                m,
928                bootstrap_module,
929                resources_state,
930            )?),
931        })
932    }
933
934    #[getter]
935    fn multiprocessing_set_start_method(&self) -> PyResult<Option<String>> {
936        if let Some(v) = &self.state.multiprocessing_set_start_method {
937            Ok(Some(v.to_string()))
938        } else {
939            Ok(None)
940        }
941    }
942
943    #[getter]
944    fn origin<'p>(&self, py: Python<'p>) -> &'p PyAny {
945        self.state
946            .get_resources_state()
947            .origin()
948            .into_py(py)
949            .into_ref(py)
950    }
951
952    #[getter]
953    fn path_hook_base_str<'p>(&self, py: Python<'p>) -> &'p PyAny {
954        self.state
955            .get_resources_state()
956            .current_exe()
957            .into_py(py)
958            .into_ref(py)
959    }
960
961    #[getter]
962    fn pkg_resources_import_auto_register(&self) -> PyResult<bool> {
963        Ok(self.state.pkg_resources_import_auto_register)
964    }
965
966    fn path_hook(slf: &PyCell<Self>, path: &PyAny) -> PyResult<OxidizedPathEntryFinder> {
967        Self::path_hook_inner(slf, path).map_err(|inner| {
968            let err = PyImportError::new_err("error running OxidizedFinder.path_hook");
969
970            if let Err(err) = err.value(slf.py()).setattr("__suppress_context__", true) {
971                err
972            } else if let Err(err) = err
973                .value(slf.py())
974                .setattr("__cause__", inner.value(slf.py()))
975            {
976                err
977            } else {
978                err
979            }
980        })
981    }
982
983    fn index_bytes(&self, py: Python, data: &PyAny) -> PyResult<()> {
984        self.state
985            .get_resources_state_mut()
986            .index_pyobject(py, data)?;
987
988        Ok(())
989    }
990
991    fn index_file_memory_mapped(&self, py: Python, path: &PyAny) -> PyResult<()> {
992        let path = pyobject_to_pathbuf(py, path)?;
993
994        self.state
995            .get_resources_state_mut()
996            .index_path_memory_mapped(path)
997            .map_err(PyValueError::new_err)?;
998
999        Ok(())
1000    }
1001
1002    fn index_interpreter_builtins(&self) -> PyResult<()> {
1003        self.state
1004            .get_resources_state_mut()
1005            .index_interpreter_builtins()
1006            .map_err(PyValueError::new_err)?;
1007
1008        Ok(())
1009    }
1010
1011    fn index_interpreter_builtin_extension_modules(&self) -> PyResult<()> {
1012        self.state
1013            .get_resources_state_mut()
1014            .index_interpreter_builtin_extension_modules()
1015            .map_err(PyValueError::new_err)?;
1016
1017        Ok(())
1018    }
1019
1020    fn index_interpreter_frozen_modules(&self) -> PyResult<()> {
1021        self.state
1022            .get_resources_state_mut()
1023            .index_interpreter_frozen_modules()
1024            .map_err(PyValueError::new_err)?;
1025
1026        Ok(())
1027    }
1028
1029    fn indexed_resources<'p>(&self, py: Python<'p>) -> PyResult<&'p PyList> {
1030        let resources_state = self.state.get_resources_state();
1031
1032        resources_state.resources_as_py_list(py)
1033    }
1034
1035    fn add_resource(&self, resource: &OxidizedResource) -> PyResult<()> {
1036        let resources_state = self.state.get_resources_state_mut();
1037
1038        resources_state
1039            .add_resource(pyobject_to_resource(resource))
1040            .map_err(|_| PyValueError::new_err("unable to add resource to finder"))?;
1041
1042        Ok(())
1043    }
1044
1045    fn add_resources(&self, resources: &PyAny) -> PyResult<()> {
1046        let resources_state = self.state.get_resources_state_mut();
1047
1048        for resource in resources.iter()? {
1049            let resource_raw = resource?;
1050            let resource = resource_raw.cast_as::<PyCell<OxidizedResource>>()?;
1051
1052            resources_state
1053                .add_resource(pyobject_to_resource(&resource.borrow()))
1054                .map_err(|_| PyValueError::new_err("unable to add resource to finder"))?;
1055        }
1056
1057        Ok(())
1058    }
1059
1060    #[args(ignore_builtin = true, ignore_frozen = true)]
1061    fn serialize_indexed_resources<'p>(
1062        &self,
1063        py: Python<'p>,
1064        ignore_builtin: bool,
1065        ignore_frozen: bool,
1066    ) -> PyResult<&'p PyBytes> {
1067        let resources_state = self.state.get_resources_state();
1068
1069        let data = resources_state
1070            .serialize_resources(ignore_builtin, ignore_frozen)
1071            .map_err(|e| PyValueError::new_err(format!("error serializing: {}", e)))?;
1072
1073        Ok(PyBytes::new(py, &data))
1074    }
1075}
1076
1077impl OxidizedFinder {
1078    fn path_hook_inner(
1079        slf: &PyCell<Self>,
1080        path_original: &PyAny,
1081    ) -> PyResult<OxidizedPathEntryFinder> {
1082        let py = slf.py();
1083        let finder = slf.borrow();
1084
1085        // We respond to the following paths:
1086        //
1087        // * self.path_hook_base_str
1088        // * virtual sub-directories under self.path_hook_base_str
1089        //
1090        // There is a mismatch between the ways that Rust and Python store paths.
1091        // self.current_exe is a Rust PathBuf and came from Rust. We can get the raw
1092        // OsString and know the raw bytes. But Python applies text encoding to
1093        // paths. Normalizing between the 2 could be difficult, especially since
1094        // Python module names can be quite literally any str value.
1095        //
1096        // We restrict accepted paths to Python str that are equal to
1097        // self.current_exe or have it + a directory separator as a strict prefix.
1098        // This leaves us with a suffix constituting the relative package path, which we
1099        // can coerce to a Rust String easily, as Python str are Unicode.
1100
1101        // Only accept str.
1102        let path = path_original.cast_as::<PyString>()?;
1103
1104        let path_hook_base = finder.path_hook_base_str(py).cast_as::<PyString>()?;
1105
1106        let target_package = if path.compare(path_hook_base)? == std::cmp::Ordering::Equal {
1107            None
1108        } else {
1109            // Accept both directory separators as prefix match.
1110            let unix_prefix = path_hook_base.call_method("__add__", ("/",), None)?;
1111            let windows_prefix = path_hook_base.call_method("__add__", ("\\",), None)?;
1112
1113            let prefix = PyTuple::new(py, [unix_prefix, windows_prefix]);
1114
1115            if !path
1116                .call_method("startswith", (prefix,), None)?
1117                .extract::<bool>()?
1118            {
1119                return Err(PyValueError::new_err(format!(
1120                    "{} is not prefixed by {}",
1121                    path.to_string_lossy(),
1122                    path_hook_base.to_string_lossy()
1123                )));
1124            }
1125
1126            // Ideally we'd strip the prefix in the domain of Python so we don't have
1127            // to worry about text encoding. However, since we need to normalize the
1128            // suffix to a Rust string anyway to facilitate filtering against UTF-8
1129            // names, we go ahead and convert to Rust/UTF-8 and do the string
1130            // operations in Rust.
1131            //
1132            // It is tempting to use os.fsencode() here, as sys.path entries are,
1133            // well, paths. But since sys.path entries are meant to map to our
1134            // path hook, we get to decide what their format is and we decide that
1135            // any unicode encoding should be in UTF-8, not whatever the current
1136            // filesystem encoding is set to. Since Rust won't handle surrogateescape
1137            // that well, we use the "replace" error handling strategy to ensure a
1138            // Rust string valid byte sequence.
1139            let path_hook_base_bytes = path_hook_base
1140                .call_method("encode", ("utf-8", "replace"), None)?
1141                .extract::<Vec<u8>>()?;
1142            let path_bytes = path
1143                .call_method("encode", ("utf-8", "replace"), None)?
1144                .extract::<Vec<u8>>()?;
1145
1146            // +1 for directory separator, which should always be 1 byte in UTF-8.
1147            let path_suffix: &[u8] = &path_bytes[path_hook_base_bytes.len() + 1..];
1148            let original_package_path = String::from_utf8(path_suffix.to_vec()).map_err(|e| {
1149                PyValueError::new_err(format!(
1150                    "error coercing package suffix to Rust string: {}",
1151                    e
1152                ))
1153            })?;
1154
1155            let package_path = original_package_path.replace('\\', "/");
1156
1157            // Ban leading and trailing directory separators.
1158            if package_path.starts_with('/') || package_path.ends_with('/') {
1159                return Err(PyValueError::new_err(
1160                    format!("rejecting virtual sub-directory because package part contains leading or trailing directory separator: {}", original_package_path)));
1161            }
1162
1163            // Ban consecutive directory separators.
1164            if package_path.contains("//") {
1165                return Err(PyValueError::new_err(format!("rejecting virtual sub-directory because it has consecutive directory separators: {}", original_package_path)));
1166            }
1167
1168            // Since we have to normalize to Python package form where dots are
1169            // special, ban dots in special places.
1170            if package_path
1171                .split('/')
1172                .any(|s| s.starts_with('.') || s.ends_with('.') || s.contains(".."))
1173            {
1174                return Err(PyValueError::new_err(
1175                    format!("rejecting virtual sub-directory because package part contains illegal dot characters: {}", original_package_path)
1176
1177                ));
1178            }
1179
1180            if package_path.is_empty() {
1181                None
1182            } else {
1183                Some(package_path.replace('/', "."))
1184            }
1185        };
1186
1187        Ok(OxidizedPathEntryFinder {
1188            finder: PyCell::new(
1189                py,
1190                OxidizedFinder {
1191                    state: finder.state.clone(),
1192                },
1193            )?
1194            .into(),
1195            source_path: path.into_py(py),
1196            target_package,
1197        })
1198    }
1199}
1200
1201/// Path-like object facilitating Python resource access.
1202///
1203/// This implements importlib.abc.Traversable.
1204#[pyclass(module = "oxidized_importer")]
1205pub(crate) struct PyOxidizerTraversable {
1206    state: Arc<ImporterState>,
1207    path: String,
1208}
1209
1210#[pymethods]
1211impl PyOxidizerTraversable {
1212    /// Yield Traversable objects in self.
1213    fn iterdir(&self) -> PyResult<&PyAny> {
1214        unimplemented!()
1215    }
1216
1217    /// Read contents of self as bytes.
1218    fn read_bytes(&self) -> PyResult<&PyAny> {
1219        unimplemented!()
1220    }
1221
1222    /// Read contents of self as text.
1223    fn read_text(&self) -> PyResult<&PyAny> {
1224        unimplemented!()
1225    }
1226
1227    /// Return True if self is a dir.
1228    fn is_dir(&self) -> PyResult<bool> {
1229        // We are a directory if the current path is a known package.
1230        // TODO We may need to expand this definition in the future to cover
1231        // virtual subdirectories in addressable resources. But this will require
1232        // changes to the resources data format to capture said annotations.
1233        if let Some(entry) = self
1234            .state
1235            .get_resources_state()
1236            .resolve_importable_module(&self.path, self.state.optimize_level)
1237        {
1238            if entry.is_package {
1239                return Ok(true);
1240            }
1241        }
1242
1243        Ok(false)
1244    }
1245
1246    /// Return True if self is a file.
1247    fn is_file(&self) -> PyResult<&PyAny> {
1248        unimplemented!()
1249    }
1250
1251    /// Return Traversable child in self.
1252    #[allow(unused)]
1253    fn joinpath(&self, child: &PyAny) -> PyResult<&PyAny> {
1254        unimplemented!()
1255    }
1256
1257    /// Return Traversable child in self.
1258    #[allow(unused)]
1259    fn __truediv__(&self, child: &PyAny) -> PyResult<&PyAny> {
1260        unimplemented!()
1261    }
1262
1263    /// mode may be 'r' or 'rb' to open as text or binary. Return a handle
1264    /// suitable for reading (same as pathlib.Path.open).
1265    ///
1266    /// When opening as text, accepts encoding parameters such as those
1267    /// accepted by io.TextIOWrapper.
1268    #[allow(unused)]
1269    #[args(py_args = "*", py_kwargs = "**")]
1270    fn open(&self, py_args: &PyTuple, py_kwargs: Option<&PyDict>) -> PyResult<&PyAny> {
1271        unimplemented!()
1272    }
1273}
1274
1275/// Replace all meta path importers with an OxidizedFinder instance and return it.
1276///
1277/// This is called after PyInit_* to finish the initialization of the
1278/// module. Its state struct is updated.
1279///
1280/// The [OxidizedFinder] is guaranteed to be on `sys.meta_path[0]` after successful
1281/// completion.
1282pub fn replace_meta_path_importers<'a, 'p>(
1283    py: Python<'p>,
1284    oxidized_importer: &PyModule,
1285    resources_state: Box<PythonResourcesState<'a, u8>>,
1286    importer_state_callback: Option<impl FnOnce(&mut ImporterState)>,
1287) -> PyResult<&'p PyCell<OxidizedFinder>> {
1288    let mut state = get_module_state(oxidized_importer)?;
1289
1290    let sys_module = py.import("sys")?;
1291
1292    // Construct and register our custom meta path importer. Because our meta path
1293    // importer is able to handle builtin and frozen modules, the existing meta path
1294    // importers are removed. The assumption here is that we're called very early
1295    // during startup and the 2 default meta path importers are installed.
1296    let oxidized_finder = PyCell::new(
1297        py,
1298        OxidizedFinder::new_from_module_and_resources(
1299            py,
1300            oxidized_importer,
1301            resources_state,
1302            importer_state_callback,
1303        )?,
1304    )?;
1305
1306    let meta_path_object = sys_module.getattr("meta_path")?;
1307
1308    meta_path_object.call_method0("clear")?;
1309    meta_path_object.call_method("append", (oxidized_finder,), None)?;
1310
1311    state.initialized = true;
1312
1313    Ok(oxidized_finder)
1314}
1315
1316/// Undoes the actions of `importlib._bootstrap_external` initialization.
1317///
1318/// This will remove types that aren't defined by this extension from
1319/// `sys.meta_path` and `sys.path_hooks`.
1320pub fn remove_external_importers(sys_module: &PyModule) -> PyResult<()> {
1321    let meta_path = sys_module.getattr("meta_path")?;
1322    let meta_path = meta_path.cast_as::<PyList>()?;
1323
1324    // We need to mutate the lists in place so any updates are reflected
1325    // in references to the lists.
1326
1327    let mut oxidized_path_hooks = vec![];
1328    let mut index = 0;
1329    while index < meta_path.len() {
1330        let entry = meta_path.get_item(index as _)?;
1331
1332        // We want to preserve `_frozen_importlib.BuiltinImporter` and
1333        // `_frozen_importlib.FrozenImporter`, if present. Ideally we'd
1334        // do PyType comparisons. However, there doesn't appear to be a way
1335        // to easily get a handle on their PyType. We'd also prefer to do
1336        // PyType.name() checks. But both these report `type`. So we key
1337        // off `__module__` instead.
1338
1339        // TODO perform type comparison natively once OxidizedFinder is defined via pyo3.
1340        if entry.get_type().to_string().contains("OxidizedFinder") {
1341            oxidized_path_hooks.push(entry.getattr("path_hook")?);
1342            index += 1;
1343        } else if entry
1344            .getattr("__module__")?
1345            .cast_as::<PyString>()?
1346            .to_string_lossy()
1347            == "_frozen_importlib"
1348        {
1349            index += 1;
1350        } else {
1351            meta_path.call_method1("pop", (index,))?;
1352        }
1353    }
1354
1355    let path_hooks = sys_module.getattr("path_hooks")?;
1356    let path_hooks = path_hooks.cast_as::<PyList>()?;
1357
1358    let mut index = 0;
1359    while index < path_hooks.len() {
1360        let entry = path_hooks.get_item(index as _)?;
1361
1362        let mut found = false;
1363        for candidate in oxidized_path_hooks.iter() {
1364            if candidate.eq(entry)? {
1365                found = true;
1366                break;
1367            }
1368        }
1369        if found {
1370            index += 1;
1371        } else {
1372            path_hooks.call_method1("pop", (index,))?;
1373        }
1374    }
1375
1376    Ok(())
1377}
1378
1379/// Prepend a path hook to [`sys.path_hooks`] that works with [OxidizedFinder].
1380///
1381/// `sys` must be a reference to the [`sys`] module.
1382///
1383/// [`sys.path_hooks`]: https://docs.python.org/3/library/sys.html#sys.path_hooks
1384/// [`sys`]: https://docs.python.org/3/library/sys.html
1385pub fn install_path_hook(finder: &PyAny, sys: &PyModule) -> PyResult<()> {
1386    let hook = finder.getattr("path_hook")?;
1387    let path_hooks = sys.getattr("path_hooks")?;
1388    path_hooks
1389        .call_method("insert", (0, hook), None)
1390        .map(|_| ())
1391}