pykeramics 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ [package]
2
+ authors.workspace = true
3
+ description = "Keramics Python bindings"
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ name = "keramics-python"
7
+ repository.workspace = true
8
+ version.workspace = true
9
+
10
+ [lib]
11
+ name = "pykeramics"
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ keramics-datetime = { version = "0.0.0", path = "../keramics-datetime" }
16
+ keramics-vfs = { version = "0.0.0", path = "../keramics-vfs" }
17
+ pyo3 = { version = "0.26.0", features = ["extension-module"] }
@@ -0,0 +1,2 @@
1
+ include Cargo.toml pyproject.toml
2
+ recursive-include src *
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: pykeramics
3
+ Version: 0.0.0
4
+ License: "Apache-2.0"
5
+ Classifier: Development Status :: 2 - Pre-Alpha
6
+ Classifier: Programming Language :: Python
7
+ Classifier: Programming Language :: Rust
8
+ Requires-Python: >=3.8
@@ -0,0 +1,2 @@
1
+ [build-system]
2
+ requires = ["setuptools>=41.0.0", "wheel", "setuptools_rust>=1.0.0"]
@@ -0,0 +1,16 @@
1
+ [metadata]
2
+ name = pykeramics
3
+ version = 0.0.0
4
+ license = "Apache-2.0"
5
+ classifiers =
6
+ Development Status :: 2 - Pre-Alpha
7
+ Programming Language :: Python
8
+ Programming Language :: Rust
9
+
10
+ [options]
11
+ python_requires = >=3.8
12
+
13
+ [egg_info]
14
+ tag_build =
15
+ tag_date = 0
16
+
@@ -0,0 +1,25 @@
1
+ # Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License. You may
5
+ # obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ # License for the specific language governing permissions and limitations
11
+ # under the License.
12
+
13
+ import os
14
+
15
+ from setuptools import setup
16
+ from setuptools_rust import RustExtension
17
+
18
+ setup(
19
+ rust_extensions=[
20
+ RustExtension(
21
+ "pykeramics",
22
+ debug=os.environ.get("BUILD_DEBUG") == "1",
23
+ )
24
+ ],
25
+ )
@@ -0,0 +1,113 @@
1
+ /* Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may
5
+ * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software
8
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ * License for the specific language governing permissions and limitations
11
+ * under the License.
12
+ */
13
+
14
+ use pyo3::exceptions::PyRuntimeError;
15
+ use pyo3::prelude::*;
16
+ use pyo3::types::{PyAny, PyNone};
17
+
18
+ use keramics_datetime::{DateTime, Filetime, PosixTime32, PosixTime64Ns};
19
+
20
+ pub struct PyDateTime {}
21
+
22
+ impl PyDateTime {
23
+ pub fn new(date_time: &DateTime) -> PyResult<Py<PyAny>> {
24
+ Python::attach(|py| -> PyResult<_> {
25
+ match date_time {
26
+ DateTime::Filetime(filetime) => {
27
+ let py_filetime: PyFiletime = PyFiletime {
28
+ filetime: filetime.clone(),
29
+ };
30
+ Ok(Py::new(py, py_filetime)?.into_any())
31
+ }
32
+ DateTime::NotSet => {
33
+ let py_none: Borrowed<'_, '_, PyNone> = PyNone::get(py);
34
+ Ok(py_none.to_owned().unbind().into_any())
35
+ }
36
+ DateTime::PosixTime32(posix_time) => {
37
+ let py_posix_time: PyPosixTime32 = PyPosixTime32 {
38
+ posix_time: posix_time.clone(),
39
+ };
40
+ Ok(Py::new(py, py_posix_time)?.into_any())
41
+ }
42
+ DateTime::PosixTime64Ns(posix_time) => {
43
+ let py_posix_time: PyPosixTime64Ns = PyPosixTime64Ns {
44
+ posix_time: posix_time.clone(),
45
+ };
46
+ Ok(Py::new(py, py_posix_time)?.into_any())
47
+ }
48
+ _ => {
49
+ todo!();
50
+ }
51
+ }
52
+ })
53
+ }
54
+ }
55
+
56
+ #[pyclass]
57
+ #[pyo3(name = "Filetime")]
58
+ #[derive(Clone)]
59
+ struct PyFiletime {
60
+ filetime: Filetime,
61
+ }
62
+
63
+ #[pymethods]
64
+ impl PyFiletime {
65
+ #[getter]
66
+ pub fn timestamp(&self) -> PyResult<u64> {
67
+ Ok(self.filetime.timestamp)
68
+ }
69
+ }
70
+
71
+ #[pyclass]
72
+ #[pyo3(name = "PosixTime32")]
73
+ #[derive(Clone)]
74
+ struct PyPosixTime32 {
75
+ posix_time: PosixTime32,
76
+ }
77
+
78
+ #[pymethods]
79
+ impl PyPosixTime32 {
80
+ #[getter]
81
+ pub fn timestamp(&self) -> PyResult<i32> {
82
+ Ok(self.posix_time.timestamp)
83
+ }
84
+ }
85
+
86
+ #[pyclass]
87
+ #[pyo3(name = "PosixTime64Ns")]
88
+ #[derive(Clone)]
89
+ struct PyPosixTime64Ns {
90
+ posix_time: PosixTime64Ns,
91
+ }
92
+
93
+ #[pymethods]
94
+ impl PyPosixTime64Ns {
95
+ #[getter]
96
+ pub fn fraction(&self) -> PyResult<u32> {
97
+ Ok(self.posix_time.fraction)
98
+ }
99
+
100
+ #[getter]
101
+ pub fn timestamp(&self) -> PyResult<i64> {
102
+ Ok(self.posix_time.timestamp)
103
+ }
104
+ }
105
+
106
+ #[pymodule]
107
+ pub fn datetime(module: &Bound<'_, PyModule>) -> PyResult<()> {
108
+ module.add_class::<PyFiletime>()?;
109
+ module.add_class::<PyPosixTime32>()?;
110
+ module.add_class::<PyPosixTime64Ns>()?;
111
+
112
+ Ok(())
113
+ }
@@ -0,0 +1,34 @@
1
+ /* Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may
5
+ * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software
8
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ * License for the specific language governing permissions and limitations
11
+ * under the License.
12
+ */
13
+
14
+ use pyo3::prelude::*;
15
+ use pyo3::types::PyDict;
16
+ use pyo3::wrap_pymodule;
17
+
18
+ mod datetime;
19
+ mod vfs;
20
+
21
+ /// Keramics Python module.
22
+ #[pymodule]
23
+ fn pykeramics(python: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
24
+ module.add("__version__", env!("CARGO_PKG_VERSION"))?;
25
+ module.add_wrapped(wrap_pymodule!(datetime::datetime))?;
26
+ module.add_wrapped(wrap_pymodule!(vfs::vfs))?;
27
+
28
+ let sys = PyModule::import(python, "sys")?;
29
+ let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?;
30
+ sys_modules.set_item("pykeramics.datetime", module.getattr("datetime")?)?;
31
+ sys_modules.set_item("pykeramics.vfs", module.getattr("vfs")?)?;
32
+
33
+ Ok(())
34
+ }
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: pykeramics
3
+ Version: 0.0.0
4
+ License: "Apache-2.0"
5
+ Classifier: Development Status :: 2 - Pre-Alpha
6
+ Classifier: Programming Language :: Python
7
+ Classifier: Programming Language :: Rust
8
+ Requires-Python: >=3.8
@@ -0,0 +1,14 @@
1
+ Cargo.toml
2
+ MANIFEST.in
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ src/datetime.rs
7
+ src/lib.rs
8
+ src/vfs.rs
9
+ src/pykeramics.egg-info/PKG-INFO
10
+ src/pykeramics.egg-info/SOURCES.txt
11
+ src/pykeramics.egg-info/dependency_links.txt
12
+ src/pykeramics.egg-info/top_level.txt
13
+ tests/test_module.py
14
+ tests/test_vfs.py
@@ -0,0 +1,296 @@
1
+ /* Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may
5
+ * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software
8
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+ * License for the specific language governing permissions and limitations
11
+ * under the License.
12
+ */
13
+
14
+ use std::sync::Arc;
15
+
16
+ use pyo3::exceptions::PyRuntimeError;
17
+ use pyo3::prelude::*;
18
+
19
+ use keramics_vfs::{
20
+ VfsFileEntry, VfsFileSystemReference, VfsFileType, VfsLocation, VfsPath, VfsResolver,
21
+ VfsResolverReference, VfsString, VfsType,
22
+ };
23
+
24
+ use super::datetime::PyDateTime;
25
+
26
+ #[pyclass]
27
+ #[pyo3(name = "VfsFileEntry")]
28
+ #[derive(Clone)]
29
+ struct PyVfsFileEntry {
30
+ file_entry: Arc<VfsFileEntry>,
31
+ }
32
+
33
+ #[pymethods]
34
+ impl PyVfsFileEntry {
35
+ #[getter]
36
+ pub fn access_time(&self) -> PyResult<Option<Py<PyAny>>> {
37
+ match self.file_entry.get_access_time() {
38
+ Some(date_time) => Ok(Some(PyDateTime::new(date_time)?)),
39
+ None => Ok(None),
40
+ }
41
+ }
42
+
43
+ #[getter]
44
+ pub fn change_time(&self) -> PyResult<Option<Py<PyAny>>> {
45
+ match self.file_entry.get_change_time() {
46
+ Some(date_time) => Ok(Some(PyDateTime::new(date_time)?)),
47
+ None => Ok(None),
48
+ }
49
+ }
50
+
51
+ #[getter]
52
+ pub fn creation_time(&self) -> PyResult<Option<Py<PyAny>>> {
53
+ match self.file_entry.get_creation_time() {
54
+ Some(date_time) => Ok(Some(PyDateTime::new(date_time)?)),
55
+ None => Ok(None),
56
+ }
57
+ }
58
+
59
+ #[getter]
60
+ pub fn name(&self) -> PyResult<Option<PyVfsString>> {
61
+ match self.file_entry.get_name() {
62
+ Some(name) => Ok(Some(PyVfsString {
63
+ string: Arc::new(name),
64
+ })),
65
+ None => Ok(None),
66
+ }
67
+ }
68
+
69
+ #[getter]
70
+ pub fn modification_time(&self) -> PyResult<Option<Py<PyAny>>> {
71
+ match self.file_entry.get_modification_time() {
72
+ Some(date_time) => Ok(Some(PyDateTime::new(date_time)?)),
73
+ None => Ok(None),
74
+ }
75
+ }
76
+ }
77
+
78
+ #[pyclass]
79
+ #[pyo3(name = "VfsFileSystem")]
80
+ #[derive(Clone)]
81
+ struct PyVfsFileSystem {
82
+ file_system: VfsFileSystemReference,
83
+ }
84
+
85
+ #[pymethods]
86
+ impl PyVfsFileSystem {}
87
+
88
+ #[pyclass(eq)]
89
+ #[pyo3(name = "VfsFileType")]
90
+ #[derive(Clone, PartialEq)]
91
+ pub enum PyVfsFileType {
92
+ #[pyo3(name = "BLOCK_DEVICE")]
93
+ BlockDevice,
94
+ #[pyo3(name = "CHARACTER_DEVICE")]
95
+ CharacterDevice,
96
+ #[pyo3(name = "DEVICE")]
97
+ Device,
98
+ #[pyo3(name = "DIRECTORY")]
99
+ Directory,
100
+ #[pyo3(name = "FILE")]
101
+ File,
102
+ #[pyo3(name = "NAMED_PIPE")]
103
+ NamedPipe,
104
+ #[pyo3(name = "SOCKET")]
105
+ Socket,
106
+ #[pyo3(name = "SYMBOLIC_LINK")]
107
+ SymbolicLink,
108
+ #[pyo3(name = "WHITEOUT")]
109
+ Whiteout,
110
+ }
111
+
112
+ #[pyclass]
113
+ #[pyo3(name = "VfsLocation")]
114
+ #[derive(Clone)]
115
+ struct PyVfsLocation {
116
+ location: Arc<VfsLocation>,
117
+ }
118
+
119
+ #[pymethods]
120
+ impl PyVfsLocation {
121
+ #[new]
122
+ #[pyo3(signature = (path_type, path))]
123
+ pub fn new(path_type: PyVfsType, path: PyVfsPath) -> PyResult<Self> {
124
+ let vfs_type: VfsType = match &path_type {
125
+ PyVfsType::Apm => VfsType::Apm,
126
+ PyVfsType::Ext => VfsType::Ext,
127
+ PyVfsType::Ewf => VfsType::Ewf,
128
+ PyVfsType::Fake => VfsType::Fake,
129
+ PyVfsType::Gpt => VfsType::Gpt,
130
+ PyVfsType::Mbr => VfsType::Mbr,
131
+ PyVfsType::Os => VfsType::Os,
132
+ PyVfsType::Qcow => VfsType::Qcow,
133
+ PyVfsType::Vhd => VfsType::Vhd,
134
+ PyVfsType::Vhdx => VfsType::Vhdx,
135
+ };
136
+ let vfs_path: &VfsPath = path.path.as_ref();
137
+ Ok(Self {
138
+ location: Arc::new(VfsLocation::new_base(&vfs_type, vfs_path.clone())),
139
+ })
140
+ }
141
+
142
+ pub fn new_with_layer(&self, path_type: PyVfsType, path: PyVfsPath) -> PyResult<Self> {
143
+ let vfs_type: VfsType = match &path_type {
144
+ PyVfsType::Apm => VfsType::Apm,
145
+ PyVfsType::Ext => VfsType::Ext,
146
+ PyVfsType::Ewf => VfsType::Ewf,
147
+ PyVfsType::Fake => VfsType::Fake,
148
+ PyVfsType::Gpt => VfsType::Gpt,
149
+ PyVfsType::Mbr => VfsType::Mbr,
150
+ PyVfsType::Os => VfsType::Os,
151
+ PyVfsType::Qcow => VfsType::Qcow,
152
+ PyVfsType::Vhd => VfsType::Vhd,
153
+ PyVfsType::Vhdx => VfsType::Vhdx,
154
+ };
155
+ let vfs_path: &VfsPath = path.path.as_ref();
156
+ Ok(Self {
157
+ location: Arc::new(self.location.new_with_layer(&vfs_type, vfs_path.clone())),
158
+ })
159
+ }
160
+ }
161
+
162
+ #[pyclass]
163
+ #[pyo3(name = "VfsResolver")]
164
+ #[derive(Clone)]
165
+ struct PyVfsResolver {
166
+ resolver: VfsResolverReference,
167
+ }
168
+
169
+ #[pymethods]
170
+ impl PyVfsResolver {
171
+ #[new]
172
+ pub fn new() -> PyResult<Self> {
173
+ Ok(Self {
174
+ resolver: VfsResolver::current(),
175
+ })
176
+ }
177
+
178
+ pub fn get_file_entry_by_location(
179
+ &self,
180
+ location: PyVfsLocation,
181
+ ) -> PyResult<Option<PyVfsFileEntry>> {
182
+ match self.resolver.get_file_entry_by_location(&location.location) {
183
+ Ok(result) => match result {
184
+ Some(file_entry) => Ok(Some(PyVfsFileEntry {
185
+ file_entry: Arc::new(file_entry),
186
+ })),
187
+ None => {
188
+ return Ok(None);
189
+ }
190
+ },
191
+ Err(error) => {
192
+ return Err(PyErr::new::<PyRuntimeError, String>(format!(
193
+ "Unable to retrieve file entry with error: {}",
194
+ error.to_string()
195
+ )));
196
+ }
197
+ }
198
+ }
199
+
200
+ pub fn open_file_system(&self, location: PyVfsLocation) -> PyResult<PyVfsFileSystem> {
201
+ match self.resolver.open_file_system(&location.location) {
202
+ Ok(file_system) => Ok(PyVfsFileSystem {
203
+ file_system: file_system,
204
+ }),
205
+ Err(error) => {
206
+ return Err(PyErr::new::<PyRuntimeError, String>(format!(
207
+ "Unable to open file system with error: {}",
208
+ error.to_string()
209
+ )));
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ #[pyclass]
216
+ #[pyo3(name = "VfsString")]
217
+ #[derive(Clone)]
218
+ struct PyVfsString {
219
+ string: Arc<VfsString>,
220
+ }
221
+
222
+ #[pymethods]
223
+ impl PyVfsString {
224
+ pub fn to_string(&self) -> String {
225
+ self.string.to_string()
226
+ }
227
+ }
228
+
229
+ #[pyclass]
230
+ #[pyo3(name = "VfsPath")]
231
+ #[derive(Clone)]
232
+ struct PyVfsPath {
233
+ path: Arc<VfsPath>,
234
+ }
235
+
236
+ #[pymethods]
237
+ impl PyVfsPath {
238
+ #[new]
239
+ #[pyo3(signature = (path_type, path))]
240
+ pub fn new(path_type: PyVfsType, path: &str) -> PyResult<Self> {
241
+ let vfs_type: VfsType = match &path_type {
242
+ PyVfsType::Apm => VfsType::Apm,
243
+ PyVfsType::Ext => VfsType::Ext,
244
+ PyVfsType::Ewf => VfsType::Ewf,
245
+ PyVfsType::Fake => VfsType::Fake,
246
+ PyVfsType::Gpt => VfsType::Gpt,
247
+ PyVfsType::Mbr => VfsType::Mbr,
248
+ PyVfsType::Os => VfsType::Os,
249
+ PyVfsType::Qcow => VfsType::Qcow,
250
+ PyVfsType::Vhd => VfsType::Vhd,
251
+ PyVfsType::Vhdx => VfsType::Vhdx,
252
+ };
253
+ Ok(Self {
254
+ path: Arc::new(VfsPath::from_path(&vfs_type, path)),
255
+ })
256
+ }
257
+ }
258
+
259
+ #[pyclass(eq)]
260
+ #[pyo3(name = "VfsType")]
261
+ #[derive(Clone, PartialEq)]
262
+ pub enum PyVfsType {
263
+ #[pyo3(name = "APM")]
264
+ Apm,
265
+ #[pyo3(name = "EXT")]
266
+ Ext,
267
+ #[pyo3(name = "EWF")]
268
+ Ewf,
269
+ #[pyo3(name = "FAKE")]
270
+ Fake,
271
+ #[pyo3(name = "GPT")]
272
+ Gpt,
273
+ #[pyo3(name = "MBR")]
274
+ Mbr,
275
+ #[pyo3(name = "OS")]
276
+ Os,
277
+ #[pyo3(name = "QCOW")]
278
+ Qcow,
279
+ #[pyo3(name = "VHD")]
280
+ Vhd,
281
+ #[pyo3(name = "VHDX")]
282
+ Vhdx,
283
+ }
284
+
285
+ #[pymodule]
286
+ pub fn vfs(module: &Bound<'_, PyModule>) -> PyResult<()> {
287
+ module.add_class::<PyVfsFileEntry>()?;
288
+ module.add_class::<PyVfsFileSystem>()?;
289
+ module.add_class::<PyVfsLocation>()?;
290
+ module.add_class::<PyVfsPath>()?;
291
+ module.add_class::<PyVfsResolver>()?;
292
+ module.add_class::<PyVfsString>()?;
293
+ module.add_class::<PyVfsType>()?;
294
+
295
+ Ok(())
296
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License. You may
7
+ # obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ # License for the specific language governing permissions and limitations
13
+ # under the License.
14
+
15
+ import pykeramics
16
+
17
+
18
+ def test_version() -> None:
19
+ assert pykeramics.__version__ == "0.0.0"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License. You may
7
+ # obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ # License for the specific language governing permissions and limitations
13
+ # under the License.
14
+
15
+ import pytest
16
+
17
+ from pykeramics import datetime
18
+ from pykeramics import vfs
19
+
20
+
21
+ def test_get_file_entry_by_location() -> None:
22
+ resolver = vfs.VfsResolver()
23
+
24
+ os_location = vfs.VfsLocation(
25
+ vfs.VfsType.OS, vfs.VfsPath(vfs.VfsType.OS, "../test_data/qcow/ext2.qcow2")
26
+ )
27
+ qcow_location = os_location.new_with_layer(
28
+ vfs.VfsType.QCOW, vfs.VfsPath(vfs.VfsType.QCOW, "/qcow1")
29
+ )
30
+ ext_location = qcow_location.new_with_layer(
31
+ vfs.VfsType.EXT, vfs.VfsPath(vfs.VfsType.EXT, "/testdir1/testfile1")
32
+ )
33
+ file_entry = resolver.get_file_entry_by_location(ext_location)
34
+
35
+ assert file_entry is not None
36
+ assert file_entry.name.to_string() == "testfile1"
37
+ assert file_entry.access_time.timestamp == 1735977482
38
+ assert file_entry.change_time.timestamp == 1735977481
39
+ assert file_entry.creation_time is None
40
+ assert file_entry.modification_time.timestamp == 1735977481
41
+
42
+ ext_location = qcow_location.new_with_layer(
43
+ vfs.VfsType.EXT, vfs.VfsPath(vfs.VfsType.EXT, "/bogus")
44
+ )
45
+ file_entry = resolver.get_file_entry_by_location(ext_location)
46
+
47
+ assert file_entry is None
48
+
49
+
50
+ def test_open_file_system() -> None:
51
+ resolver = vfs.VfsResolver()
52
+
53
+ os_location = vfs.VfsLocation(
54
+ vfs.VfsType.OS, vfs.VfsPath(vfs.VfsType.OS, "../test_data/qcow/ext2.qcow2")
55
+ )
56
+ qcow_location = os_location.new_with_layer(
57
+ vfs.VfsType.QCOW, vfs.VfsPath(vfs.VfsType.QCOW, "/qcow1")
58
+ )
59
+ ext_location = qcow_location.new_with_layer(
60
+ vfs.VfsType.EXT, vfs.VfsPath(vfs.VfsType.EXT, "/testdir1/testfile1")
61
+ )
62
+ file_system = resolver.open_file_system(ext_location)
63
+
64
+ assert file_system is not None