oaknut-file 1.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.
- oaknut_file-1.0.0/LICENSE +21 -0
- oaknut_file-1.0.0/PKG-INFO +189 -0
- oaknut_file-1.0.0/README.md +167 -0
- oaknut_file-1.0.0/pyproject.toml +60 -0
- oaknut_file-1.0.0/setup.cfg +4 -0
- oaknut_file-1.0.0/src/oaknut/file/__init__.py +62 -0
- oaknut_file-1.0.0/src/oaknut/file/access.py +68 -0
- oaknut_file-1.0.0/src/oaknut/file/filename_encoding.py +73 -0
- oaknut_file-1.0.0/src/oaknut/file/formats.py +28 -0
- oaknut_file-1.0.0/src/oaknut/file/inf.py +137 -0
- oaknut_file-1.0.0/src/oaknut/file/meta.py +49 -0
- oaknut_file-1.0.0/src/oaknut/file/xattr.py +156 -0
- oaknut_file-1.0.0/src/oaknut_file.egg-info/PKG-INFO +189 -0
- oaknut_file-1.0.0/src/oaknut_file.egg-info/SOURCES.txt +21 -0
- oaknut_file-1.0.0/src/oaknut_file.egg-info/dependency_links.txt +1 -0
- oaknut_file-1.0.0/src/oaknut_file.egg-info/requires.txt +3 -0
- oaknut_file-1.0.0/src/oaknut_file.egg-info/top_level.txt +1 -0
- oaknut_file-1.0.0/tests/test_access.py +104 -0
- oaknut_file-1.0.0/tests/test_filename_encoding.py +101 -0
- oaknut_file-1.0.0/tests/test_formats.py +41 -0
- oaknut_file-1.0.0/tests/test_inf.py +136 -0
- oaknut_file-1.0.0/tests/test_meta.py +63 -0
- oaknut_file-1.0.0/tests/test_xattr.py +149 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Smallshire
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oaknut-file
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Acorn file metadata handling: INF sidecars, filename encoding, xattrs, and access flags
|
|
5
|
+
Author-email: Robert Smallshire <robert@smallshire.org.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rob-smallshire/oaknut
|
|
8
|
+
Project-URL: Repository, https://github.com/rob-smallshire/oaknut
|
|
9
|
+
Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: System :: Filesystems
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: xattr>=1.0; sys_platform == "darwin"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# oaknut-file
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/oaknut-file/)
|
|
26
|
+
[](https://github.com/rob-smallshire/oaknut-file/actions/workflows/tests.yml)
|
|
27
|
+
[](https://pypi.org/project/oaknut-file/)
|
|
28
|
+
[](https://github.com/rob-smallshire/oaknut-file/blob/master/LICENSE)
|
|
29
|
+
|
|
30
|
+
Acorn file metadata handling for the oaknut package family.
|
|
31
|
+
|
|
32
|
+
`oaknut-file` is the shared metadata layer used by
|
|
33
|
+
[`oaknut-zip`](https://github.com/rob-smallshire/oaknut-zip) and
|
|
34
|
+
[`oaknut-dfs`](https://github.com/rob-smallshire/oaknut-dfs). It provides:
|
|
35
|
+
|
|
36
|
+
- The `Access` IntFlag enum for Acorn file attribute bytes
|
|
37
|
+
- The `AcornMeta` dataclass for load/exec addresses and attributes
|
|
38
|
+
- INF sidecar file parsing and formatting (traditional and PiEconetBridge variants)
|
|
39
|
+
- Filename metadata encoding (`,xxx`, `,llllllll,eeeeeeee`, `,load-exec`)
|
|
40
|
+
- Extended attribute read/write under `user.acorn.*` and `user.econet_*`
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add oaknut-file
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`pip install oaknut-file` also works.
|
|
51
|
+
|
|
52
|
+
The `xattr` package is automatically installed on macOS, where it is
|
|
53
|
+
required for extended attribute support. Linux uses `os.setxattr` from
|
|
54
|
+
the standard library and needs no additional dependency. Windows does
|
|
55
|
+
not support extended attributes; the xattr functions will raise on use.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
### Access flags
|
|
60
|
+
|
|
61
|
+
The `Access` IntFlag enum represents the standard Acorn OSFILE attribute byte.
|
|
62
|
+
Bit values match the filing system API convention used by PiEconetBridge and
|
|
63
|
+
the `user.acorn.attr` extended attribute.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from oaknut.file import Access
|
|
67
|
+
|
|
68
|
+
# Compose flags with bitwise OR
|
|
69
|
+
flags = Access.R | Access.W | Access.L
|
|
70
|
+
print(repr(flags)) # <Access.R|W|L: 11>
|
|
71
|
+
print(hex(flags)) # 0x0B
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Flag | Value | Meaning |
|
|
75
|
+
|------|-------|---------|
|
|
76
|
+
| `Access.R` | 0x01 | Owner read |
|
|
77
|
+
| `Access.W` | 0x02 | Owner write |
|
|
78
|
+
| `Access.E` | 0x04 | Execute only |
|
|
79
|
+
| `Access.L` | 0x08 | Locked |
|
|
80
|
+
| `Access.PR` | 0x10 | Public read |
|
|
81
|
+
| `Access.PW` | 0x20 | Public write |
|
|
82
|
+
|
|
83
|
+
### INF sidecar files
|
|
84
|
+
|
|
85
|
+
Two INF sidecar formats are supported. `parse_inf_line()` auto-detects which
|
|
86
|
+
format a line uses, while `format_trad_inf_line()` and `format_pieb_inf_line()`
|
|
87
|
+
let you choose explicitly when writing.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from oaknut.file import (
|
|
91
|
+
Access, format_trad_inf_line, format_pieb_inf_line, parse_inf_line,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Traditional INF: filename load exec length [attr]
|
|
95
|
+
trad = format_trad_inf_line(
|
|
96
|
+
filename="HELLO",
|
|
97
|
+
load_addr=0x1900,
|
|
98
|
+
exec_addr=0x8023,
|
|
99
|
+
length=0x100,
|
|
100
|
+
attr=int(Access.R | Access.W),
|
|
101
|
+
)
|
|
102
|
+
print(trad)
|
|
103
|
+
# HELLO 00001900 00008023 00000100 03
|
|
104
|
+
|
|
105
|
+
# PiEconetBridge INF: owner load exec perm
|
|
106
|
+
pieb = format_pieb_inf_line(
|
|
107
|
+
load_addr=0xFFFFDD00,
|
|
108
|
+
exec_addr=0xFFFFDD00,
|
|
109
|
+
attr=int(Access.R | Access.W | Access.L | Access.PR),
|
|
110
|
+
)
|
|
111
|
+
print(pieb)
|
|
112
|
+
# 0 ffffdd00 ffffdd00 1b
|
|
113
|
+
|
|
114
|
+
# Auto-detect format on parse
|
|
115
|
+
source, meta = parse_inf_line(trad)
|
|
116
|
+
print(meta)
|
|
117
|
+
# AcornMeta(load_addr=0x1900, exec_addr=0x8023, attr=0x03)
|
|
118
|
+
|
|
119
|
+
source, meta = parse_inf_line(pieb)
|
|
120
|
+
print(meta)
|
|
121
|
+
# AcornMeta(load_addr=0xFFFFDD00, exec_addr=0xFFFFDD00, attr=0x1B, filetype=0xFDD)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Filename metadata encoding
|
|
125
|
+
|
|
126
|
+
Three filename suffix conventions are supported for embedding load/exec
|
|
127
|
+
addresses or RISC OS filetypes in host filenames.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from oaknut.file import parse_encoded_filename
|
|
131
|
+
|
|
132
|
+
# RISC OS filetype suffix (3 hex digits)
|
|
133
|
+
clean, meta = parse_encoded_filename("PROG,ffb")
|
|
134
|
+
print(clean, meta.infer_filetype())
|
|
135
|
+
# ('PROG', filetype=0xFFB)
|
|
136
|
+
|
|
137
|
+
# MOS load-exec suffix (variable-width hex)
|
|
138
|
+
clean, meta = parse_encoded_filename("PROG,1900-801f")
|
|
139
|
+
print(clean, meta.load_addr, meta.exec_addr)
|
|
140
|
+
# ('PROG', load_addr=0x1900, exec_addr=0x801F)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Filetype-stamped load addresses
|
|
144
|
+
|
|
145
|
+
When a load address has its top 12 bits set to `0xFFF`, the next 12 bits
|
|
146
|
+
encode a RISC OS filetype.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from oaknut.file import AcornMeta
|
|
150
|
+
|
|
151
|
+
meta = AcornMeta(load_addr=0xFFFF0E10)
|
|
152
|
+
print(meta.is_filetype_stamped, hex(meta.infer_filetype()))
|
|
153
|
+
# is_filetype_stamped=True, infer_filetype()=0xF0E
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Extended attributes
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from oaknut.file import write_acorn_xattrs, read_acorn_xattrs
|
|
160
|
+
|
|
161
|
+
# Write to user.acorn.* namespace
|
|
162
|
+
write_acorn_xattrs(
|
|
163
|
+
"myfile.bin",
|
|
164
|
+
load_addr=0x1900,
|
|
165
|
+
exec_addr=0x8023,
|
|
166
|
+
attr=0x03,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Read back (falls through to user.econet_* if user.acorn.* is absent)
|
|
170
|
+
meta = read_acorn_xattrs("myfile.bin")
|
|
171
|
+
print(meta.load_addr, meta.exec_addr, meta.attr)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Public API
|
|
175
|
+
|
|
176
|
+
| Module | Exports |
|
|
177
|
+
|--------|---------|
|
|
178
|
+
| `oaknut.file.access` | `Access`, `format_access_hex`, `format_access_text` |
|
|
179
|
+
| `oaknut.file.meta` | `AcornMeta` |
|
|
180
|
+
| `oaknut.file.formats` | `MetaFormat`, `SOURCE_*` labels |
|
|
181
|
+
| `oaknut.file.inf` | `parse_inf_line`, `format_trad_inf_line`, `format_pieb_inf_line`, `read_inf_file`, `write_inf_file` |
|
|
182
|
+
| `oaknut.file.filename_encoding` | `parse_encoded_filename`, `build_filename_suffix`, `build_mos_filename_suffix` |
|
|
183
|
+
| `oaknut.file.xattr` | `read_acorn_xattrs`, `write_acorn_xattrs`, `read_econet_xattrs`, `write_econet_xattrs` |
|
|
184
|
+
|
|
185
|
+
All public symbols are also re-exported from the top-level `oaknut.file` package.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# oaknut-file
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/oaknut-file/)
|
|
4
|
+
[](https://github.com/rob-smallshire/oaknut-file/actions/workflows/tests.yml)
|
|
5
|
+
[](https://pypi.org/project/oaknut-file/)
|
|
6
|
+
[](https://github.com/rob-smallshire/oaknut-file/blob/master/LICENSE)
|
|
7
|
+
|
|
8
|
+
Acorn file metadata handling for the oaknut package family.
|
|
9
|
+
|
|
10
|
+
`oaknut-file` is the shared metadata layer used by
|
|
11
|
+
[`oaknut-zip`](https://github.com/rob-smallshire/oaknut-zip) and
|
|
12
|
+
[`oaknut-dfs`](https://github.com/rob-smallshire/oaknut-dfs). It provides:
|
|
13
|
+
|
|
14
|
+
- The `Access` IntFlag enum for Acorn file attribute bytes
|
|
15
|
+
- The `AcornMeta` dataclass for load/exec addresses and attributes
|
|
16
|
+
- INF sidecar file parsing and formatting (traditional and PiEconetBridge variants)
|
|
17
|
+
- Filename metadata encoding (`,xxx`, `,llllllll,eeeeeeee`, `,load-exec`)
|
|
18
|
+
- Extended attribute read/write under `user.acorn.*` and `user.econet_*`
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv add oaknut-file
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`pip install oaknut-file` also works.
|
|
29
|
+
|
|
30
|
+
The `xattr` package is automatically installed on macOS, where it is
|
|
31
|
+
required for extended attribute support. Linux uses `os.setxattr` from
|
|
32
|
+
the standard library and needs no additional dependency. Windows does
|
|
33
|
+
not support extended attributes; the xattr functions will raise on use.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
### Access flags
|
|
38
|
+
|
|
39
|
+
The `Access` IntFlag enum represents the standard Acorn OSFILE attribute byte.
|
|
40
|
+
Bit values match the filing system API convention used by PiEconetBridge and
|
|
41
|
+
the `user.acorn.attr` extended attribute.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from oaknut.file import Access
|
|
45
|
+
|
|
46
|
+
# Compose flags with bitwise OR
|
|
47
|
+
flags = Access.R | Access.W | Access.L
|
|
48
|
+
print(repr(flags)) # <Access.R|W|L: 11>
|
|
49
|
+
print(hex(flags)) # 0x0B
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Flag | Value | Meaning |
|
|
53
|
+
|------|-------|---------|
|
|
54
|
+
| `Access.R` | 0x01 | Owner read |
|
|
55
|
+
| `Access.W` | 0x02 | Owner write |
|
|
56
|
+
| `Access.E` | 0x04 | Execute only |
|
|
57
|
+
| `Access.L` | 0x08 | Locked |
|
|
58
|
+
| `Access.PR` | 0x10 | Public read |
|
|
59
|
+
| `Access.PW` | 0x20 | Public write |
|
|
60
|
+
|
|
61
|
+
### INF sidecar files
|
|
62
|
+
|
|
63
|
+
Two INF sidecar formats are supported. `parse_inf_line()` auto-detects which
|
|
64
|
+
format a line uses, while `format_trad_inf_line()` and `format_pieb_inf_line()`
|
|
65
|
+
let you choose explicitly when writing.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from oaknut.file import (
|
|
69
|
+
Access, format_trad_inf_line, format_pieb_inf_line, parse_inf_line,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Traditional INF: filename load exec length [attr]
|
|
73
|
+
trad = format_trad_inf_line(
|
|
74
|
+
filename="HELLO",
|
|
75
|
+
load_addr=0x1900,
|
|
76
|
+
exec_addr=0x8023,
|
|
77
|
+
length=0x100,
|
|
78
|
+
attr=int(Access.R | Access.W),
|
|
79
|
+
)
|
|
80
|
+
print(trad)
|
|
81
|
+
# HELLO 00001900 00008023 00000100 03
|
|
82
|
+
|
|
83
|
+
# PiEconetBridge INF: owner load exec perm
|
|
84
|
+
pieb = format_pieb_inf_line(
|
|
85
|
+
load_addr=0xFFFFDD00,
|
|
86
|
+
exec_addr=0xFFFFDD00,
|
|
87
|
+
attr=int(Access.R | Access.W | Access.L | Access.PR),
|
|
88
|
+
)
|
|
89
|
+
print(pieb)
|
|
90
|
+
# 0 ffffdd00 ffffdd00 1b
|
|
91
|
+
|
|
92
|
+
# Auto-detect format on parse
|
|
93
|
+
source, meta = parse_inf_line(trad)
|
|
94
|
+
print(meta)
|
|
95
|
+
# AcornMeta(load_addr=0x1900, exec_addr=0x8023, attr=0x03)
|
|
96
|
+
|
|
97
|
+
source, meta = parse_inf_line(pieb)
|
|
98
|
+
print(meta)
|
|
99
|
+
# AcornMeta(load_addr=0xFFFFDD00, exec_addr=0xFFFFDD00, attr=0x1B, filetype=0xFDD)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Filename metadata encoding
|
|
103
|
+
|
|
104
|
+
Three filename suffix conventions are supported for embedding load/exec
|
|
105
|
+
addresses or RISC OS filetypes in host filenames.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from oaknut.file import parse_encoded_filename
|
|
109
|
+
|
|
110
|
+
# RISC OS filetype suffix (3 hex digits)
|
|
111
|
+
clean, meta = parse_encoded_filename("PROG,ffb")
|
|
112
|
+
print(clean, meta.infer_filetype())
|
|
113
|
+
# ('PROG', filetype=0xFFB)
|
|
114
|
+
|
|
115
|
+
# MOS load-exec suffix (variable-width hex)
|
|
116
|
+
clean, meta = parse_encoded_filename("PROG,1900-801f")
|
|
117
|
+
print(clean, meta.load_addr, meta.exec_addr)
|
|
118
|
+
# ('PROG', load_addr=0x1900, exec_addr=0x801F)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Filetype-stamped load addresses
|
|
122
|
+
|
|
123
|
+
When a load address has its top 12 bits set to `0xFFF`, the next 12 bits
|
|
124
|
+
encode a RISC OS filetype.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from oaknut.file import AcornMeta
|
|
128
|
+
|
|
129
|
+
meta = AcornMeta(load_addr=0xFFFF0E10)
|
|
130
|
+
print(meta.is_filetype_stamped, hex(meta.infer_filetype()))
|
|
131
|
+
# is_filetype_stamped=True, infer_filetype()=0xF0E
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Extended attributes
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from oaknut.file import write_acorn_xattrs, read_acorn_xattrs
|
|
138
|
+
|
|
139
|
+
# Write to user.acorn.* namespace
|
|
140
|
+
write_acorn_xattrs(
|
|
141
|
+
"myfile.bin",
|
|
142
|
+
load_addr=0x1900,
|
|
143
|
+
exec_addr=0x8023,
|
|
144
|
+
attr=0x03,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Read back (falls through to user.econet_* if user.acorn.* is absent)
|
|
148
|
+
meta = read_acorn_xattrs("myfile.bin")
|
|
149
|
+
print(meta.load_addr, meta.exec_addr, meta.attr)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Public API
|
|
153
|
+
|
|
154
|
+
| Module | Exports |
|
|
155
|
+
|--------|---------|
|
|
156
|
+
| `oaknut.file.access` | `Access`, `format_access_hex`, `format_access_text` |
|
|
157
|
+
| `oaknut.file.meta` | `AcornMeta` |
|
|
158
|
+
| `oaknut.file.formats` | `MetaFormat`, `SOURCE_*` labels |
|
|
159
|
+
| `oaknut.file.inf` | `parse_inf_line`, `format_trad_inf_line`, `format_pieb_inf_line`, `read_inf_file`, `write_inf_file` |
|
|
160
|
+
| `oaknut.file.filename_encoding` | `parse_encoded_filename`, `build_filename_suffix`, `build_mos_filename_suffix` |
|
|
161
|
+
| `oaknut.file.xattr` | `read_acorn_xattrs`, `write_acorn_xattrs`, `read_econet_xattrs`, `write_econet_xattrs` |
|
|
162
|
+
|
|
163
|
+
All public symbols are also re-exported from the top-level `oaknut.file` package.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oaknut-file"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
|
|
9
|
+
description = "Acorn file metadata handling: INF sidecars, filename encoding, xattrs, and access flags"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: System :: Filesystems",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"xattr>=1.0; sys_platform == 'darwin'",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/rob-smallshire/oaknut"
|
|
29
|
+
Repository = "https://github.com/rob-smallshire/oaknut"
|
|
30
|
+
Issues = "https://github.com/rob-smallshire/oaknut/issues"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
test = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"jinja2>=3.0",
|
|
36
|
+
]
|
|
37
|
+
dev = [
|
|
38
|
+
"bump-my-version>=0.28.0",
|
|
39
|
+
"pre-commit>=3.0",
|
|
40
|
+
{include-group = "test"},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.dynamic]
|
|
44
|
+
version = { attr = "oaknut.file.__version__" }
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.bumpversion]
|
|
50
|
+
current_version = "1.0.0"
|
|
51
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
52
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
53
|
+
tag = true
|
|
54
|
+
commit = true
|
|
55
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
56
|
+
tag_name = "oaknut-file-v{new_version}"
|
|
57
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
58
|
+
files = [
|
|
59
|
+
{ filename = "src/oaknut/file/__init__.py" },
|
|
60
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Acorn file metadata handling.
|
|
2
|
+
|
|
3
|
+
Shared metadata layer for the oaknut package family: INF sidecar
|
|
4
|
+
parsing/formatting, filename encoding schemes, extended attributes,
|
|
5
|
+
and access flag management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
|
|
10
|
+
from oaknut.file.access import Access, format_access_hex, format_access_text
|
|
11
|
+
from oaknut.file.filename_encoding import (
|
|
12
|
+
build_filename_suffix,
|
|
13
|
+
build_mos_filename_suffix,
|
|
14
|
+
parse_encoded_filename,
|
|
15
|
+
)
|
|
16
|
+
from oaknut.file.formats import (
|
|
17
|
+
SOURCE_DIR,
|
|
18
|
+
SOURCE_FILENAME,
|
|
19
|
+
SOURCE_INF_PIEB,
|
|
20
|
+
SOURCE_INF_TRAD,
|
|
21
|
+
SOURCE_SPARKFS,
|
|
22
|
+
MetaFormat,
|
|
23
|
+
)
|
|
24
|
+
from oaknut.file.inf import (
|
|
25
|
+
format_pieb_inf_line,
|
|
26
|
+
format_trad_inf_line,
|
|
27
|
+
parse_inf_line,
|
|
28
|
+
read_inf_file,
|
|
29
|
+
write_inf_file,
|
|
30
|
+
)
|
|
31
|
+
from oaknut.file.meta import AcornMeta
|
|
32
|
+
from oaknut.file.xattr import (
|
|
33
|
+
read_acorn_xattrs,
|
|
34
|
+
read_econet_xattrs,
|
|
35
|
+
write_acorn_xattrs,
|
|
36
|
+
write_econet_xattrs,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Access",
|
|
41
|
+
"AcornMeta",
|
|
42
|
+
"MetaFormat",
|
|
43
|
+
"SOURCE_DIR",
|
|
44
|
+
"SOURCE_FILENAME",
|
|
45
|
+
"SOURCE_INF_PIEB",
|
|
46
|
+
"SOURCE_INF_TRAD",
|
|
47
|
+
"SOURCE_SPARKFS",
|
|
48
|
+
"build_filename_suffix",
|
|
49
|
+
"build_mos_filename_suffix",
|
|
50
|
+
"format_access_hex",
|
|
51
|
+
"format_access_text",
|
|
52
|
+
"format_pieb_inf_line",
|
|
53
|
+
"format_trad_inf_line",
|
|
54
|
+
"parse_encoded_filename",
|
|
55
|
+
"parse_inf_line",
|
|
56
|
+
"read_acorn_xattrs",
|
|
57
|
+
"read_econet_xattrs",
|
|
58
|
+
"read_inf_file",
|
|
59
|
+
"write_acorn_xattrs",
|
|
60
|
+
"write_econet_xattrs",
|
|
61
|
+
"write_inf_file",
|
|
62
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Acorn file access attributes.
|
|
2
|
+
|
|
3
|
+
The ``Access`` IntFlag enum represents the standard Acorn OSFILE
|
|
4
|
+
attribute byte. Bit values match the filing system API convention,
|
|
5
|
+
ensuring compatibility with PiEconetBridge ``perm`` and the
|
|
6
|
+
``user.acorn.attr`` extended attribute.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import IntFlag
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Access(IntFlag):
|
|
15
|
+
"""Acorn file access attributes.
|
|
16
|
+
|
|
17
|
+
Composable with ``|``::
|
|
18
|
+
|
|
19
|
+
Access.R | Access.W | Access.L
|
|
20
|
+
Access.R | Access.W | Access.PR # with public read
|
|
21
|
+
|
|
22
|
+
The integer value of a combination is the standard Acorn
|
|
23
|
+
attribute byte, suitable for storage in xattrs or INF files::
|
|
24
|
+
|
|
25
|
+
int(Access.R | Access.W) # 0x03
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
R = 0x01 # Owner read
|
|
29
|
+
W = 0x02 # Owner write
|
|
30
|
+
E = 0x04 # Execute only
|
|
31
|
+
L = 0x08 # Locked (prevents delete, overwrite, rename)
|
|
32
|
+
PR = 0x10 # Public read
|
|
33
|
+
PW = 0x20 # Public write
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_access_hex(attr: int | None) -> str:
|
|
37
|
+
"""Format an attribute byte as a two-digit uppercase hex string.
|
|
38
|
+
|
|
39
|
+
Returns empty string for None.
|
|
40
|
+
"""
|
|
41
|
+
if attr is None:
|
|
42
|
+
return ""
|
|
43
|
+
return f"{attr:02X}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_access_text(attr: int | None) -> str:
|
|
47
|
+
"""Format attributes as a human-readable access string.
|
|
48
|
+
|
|
49
|
+
Returns ``"owner/public"`` form, e.g. ``"LWR/R"``.
|
|
50
|
+
"""
|
|
51
|
+
if attr is None:
|
|
52
|
+
return "/"
|
|
53
|
+
|
|
54
|
+
owner = ""
|
|
55
|
+
if attr & Access.L:
|
|
56
|
+
owner += "L"
|
|
57
|
+
if attr & Access.W:
|
|
58
|
+
owner += "W"
|
|
59
|
+
if attr & Access.R:
|
|
60
|
+
owner += "R"
|
|
61
|
+
|
|
62
|
+
public = ""
|
|
63
|
+
if attr & Access.PW:
|
|
64
|
+
public += "W"
|
|
65
|
+
if attr & Access.PR:
|
|
66
|
+
public += "R"
|
|
67
|
+
|
|
68
|
+
return f"{owner}/{public}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Filename metadata encoding schemes.
|
|
2
|
+
|
|
3
|
+
Three conventions for encoding Acorn file metadata in host filenames:
|
|
4
|
+
|
|
5
|
+
- **RISC OS filetype**: ``filename,xxx`` (3-digit hex filetype)
|
|
6
|
+
- **RISC OS load/exec**: ``filename,llllllll,eeeeeeee`` (8+8 digit hex)
|
|
7
|
+
- **MOS load/exec**: ``filename,load-exec`` (variable-width hex)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
from oaknut.file.meta import AcornMeta
|
|
15
|
+
|
|
16
|
+
# Regex patterns for each encoding scheme
|
|
17
|
+
SUFFIX_FILETYPE_RE = re.compile(r"^(.*),([0-9a-fA-F]{3})$")
|
|
18
|
+
SUFFIX_LOADEXEC_RE = re.compile(r"^(.*),([0-9a-fA-F]{8}),([0-9a-fA-F]{8})$")
|
|
19
|
+
SUFFIX_MOS_LOADEXEC_RE = re.compile(r"^(.*),([0-9a-fA-F]{1,8})-([0-9a-fA-F]{1,8})$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_encoded_filename(filename: str) -> tuple[str, AcornMeta | None]:
|
|
23
|
+
"""Strip metadata suffix from a filename.
|
|
24
|
+
|
|
25
|
+
Tries each encoding scheme in order: RISC OS load/exec,
|
|
26
|
+
MOS load/exec, filetype. Returns ``(clean_filename, metadata)``
|
|
27
|
+
where *metadata* is ``None`` if no encoding was found.
|
|
28
|
+
"""
|
|
29
|
+
# Try RISC OS load/exec first (most specific: 8+8 digits)
|
|
30
|
+
m = SUFFIX_LOADEXEC_RE.match(filename)
|
|
31
|
+
if m:
|
|
32
|
+
load_addr = int(m.group(2), 16)
|
|
33
|
+
exec_addr = int(m.group(3), 16)
|
|
34
|
+
return m.group(1), AcornMeta(load_addr=load_addr, exec_addr=exec_addr)
|
|
35
|
+
|
|
36
|
+
# Try MOS load/exec (variable-width with hyphen)
|
|
37
|
+
m = SUFFIX_MOS_LOADEXEC_RE.match(filename)
|
|
38
|
+
if m:
|
|
39
|
+
load_addr = int(m.group(2), 16)
|
|
40
|
+
exec_addr = int(m.group(3), 16)
|
|
41
|
+
return m.group(1), AcornMeta(load_addr=load_addr, exec_addr=exec_addr)
|
|
42
|
+
|
|
43
|
+
# Try filetype (3-digit hex)
|
|
44
|
+
m = SUFFIX_FILETYPE_RE.match(filename)
|
|
45
|
+
if m:
|
|
46
|
+
filetype = int(m.group(2), 16)
|
|
47
|
+
# Synthesise a RISC OS load address from the filetype
|
|
48
|
+
load_addr = 0xFFF00000 | (filetype << 8)
|
|
49
|
+
return m.group(1), AcornMeta(
|
|
50
|
+
load_addr=load_addr, exec_addr=0, filetype=filetype,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return filename, None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_filename_suffix(meta: AcornMeta) -> str:
|
|
57
|
+
"""Build a RISC OS filename encoding suffix.
|
|
58
|
+
|
|
59
|
+
Returns ``,xxx`` for filetype-stamped files, or
|
|
60
|
+
``,llllllll,eeeeeeee`` for literal load/exec addresses.
|
|
61
|
+
"""
|
|
62
|
+
if meta.is_filetype_stamped:
|
|
63
|
+
ft = meta.infer_filetype()
|
|
64
|
+
return f",{ft:03x}"
|
|
65
|
+
return f",{meta.load_addr:08x},{meta.exec_addr:08x}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_mos_filename_suffix(meta: AcornMeta) -> str:
|
|
69
|
+
"""Build a MOS filename encoding suffix.
|
|
70
|
+
|
|
71
|
+
Returns ``,load-exec`` with variable-width lowercase hex.
|
|
72
|
+
"""
|
|
73
|
+
return f",{meta.load_addr:x}-{meta.exec_addr:x}"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Metadata format and source labels.
|
|
2
|
+
|
|
3
|
+
``MetaFormat`` enumerates the output formats for writing file metadata
|
|
4
|
+
to the host filesystem. The ``SOURCE_*`` constants label where
|
|
5
|
+
metadata was obtained from.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MetaFormat(str, Enum):
|
|
14
|
+
"""Supported metadata output formats."""
|
|
15
|
+
|
|
16
|
+
INF_TRAD = "inf-trad"
|
|
17
|
+
INF_PIEB = "inf-pieb"
|
|
18
|
+
XATTR_ACORN = "xattr-acorn"
|
|
19
|
+
XATTR_PIEB = "xattr-pieb"
|
|
20
|
+
FILENAME_RISCOS = "filename-riscos"
|
|
21
|
+
FILENAME_MOS = "filename-mos"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
SOURCE_SPARKFS = "sparkfs"
|
|
25
|
+
SOURCE_INF_TRAD = "inf-trad"
|
|
26
|
+
SOURCE_INF_PIEB = "inf-pieb"
|
|
27
|
+
SOURCE_FILENAME = "filename"
|
|
28
|
+
SOURCE_DIR = "dir"
|