Skip to content

Commit a3a43e8

Browse files
committed
Improve mmap behavior and tests
1 parent d91a2a4 commit a3a43e8

26 files changed

+527
-5
lines changed

.github/workflows/release.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
concurrency:
8+
group: release
9+
cancel-in-progress: false
10+
11+
jobs:
12+
release:
13+
name: Release (on push to main)
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: write
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Compute next tag
24+
id: version
25+
shell: bash
26+
run: |
27+
set -euo pipefail
28+
latest_tag="$(git tag --list --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)"
29+
if [[ -z "${latest_tag}" ]]; then
30+
next_tag="1.0.0"
31+
else
32+
IFS='.' read -r major minor patch <<< "${latest_tag}"
33+
patch=$((patch + 1))
34+
next_tag="${major}.${minor}.${patch}"
35+
fi
36+
echo "next_tag=${next_tag}" >> "${GITHUB_OUTPUT}"
37+
38+
- name: Package skill artifact
39+
shell: bash
40+
run: |
41+
set -euo pipefail
42+
python3 - <<'PY'
43+
from pathlib import Path
44+
import zipfile
45+
import sys
46+
47+
skill_path = Path("skills/STFilePath").resolve()
48+
out_dir = Path("skills/dist").resolve()
49+
skill_md = skill_path / "SKILL.md"
50+
51+
if not skill_md.exists():
52+
print(f"[ERROR] SKILL.md not found at {skill_md}")
53+
sys.exit(1)
54+
55+
lines = skill_md.read_text(encoding="utf-8").lstrip("\ufeff").splitlines()
56+
if not lines or lines[0].strip() != "---":
57+
print("[ERROR] Missing YAML frontmatter in SKILL.md (no opening ---)")
58+
sys.exit(1)
59+
try:
60+
end_index = lines.index("---", 1)
61+
except ValueError:
62+
print("[ERROR] Missing YAML frontmatter in SKILL.md (no closing ---)")
63+
sys.exit(1)
64+
65+
frontmatter = lines[1:end_index]
66+
def get_field(name: str):
67+
prefix = f"{name}:"
68+
for line in frontmatter:
69+
if line.strip().startswith(prefix):
70+
return line.split(":", 1)[1].strip().strip('"').strip("'")
71+
return None
72+
73+
name = get_field("name")
74+
description = get_field("description")
75+
if not name or not description:
76+
print("[ERROR] SKILL.md frontmatter must include name and description")
77+
sys.exit(1)
78+
79+
out_dir.mkdir(parents=True, exist_ok=True)
80+
skill_filename = out_dir / f"{skill_path.name}.skill"
81+
skip_names = {".DS_Store"}
82+
83+
with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
84+
for file_path in skill_path.rglob("*"):
85+
if file_path.is_file():
86+
if file_path.name in skip_names:
87+
continue
88+
if any(part in ("__MACOSX",) for part in file_path.parts):
89+
continue
90+
arcname = file_path.relative_to(skill_path.parent)
91+
zipf.write(file_path, arcname)
92+
print(f" Added: {arcname}")
93+
94+
print(f"\n[OK] Successfully packaged skill to: {skill_filename}")
95+
PY
96+
97+
- name: Create GitHub Release
98+
uses: softprops/action-gh-release@v2
99+
with:
100+
tag_name: ${{ steps.version.outputs.next_tag }}
101+
name: Release ${{ steps.version.outputs.next_tag }}
102+
generate_release_notes: true
103+
files: |
104+
skills/dist/STFilePath.skill

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,31 @@ Task {
129129
}
130130
```
131131

132+
### Memory Mapping (mmap)
133+
134+
`STFile` supports scoped memory mapping for fast reads/writes.
135+
136+
```swift
137+
import STFilePath
138+
139+
let file = STFile("path/to/data.bin")
140+
try file.setSize(4096)
141+
142+
try file.withMmap { mmap in
143+
let data = mmap.read()
144+
print("bytes:", data.count)
145+
try mmap.write(Data([0x01, 0x02, 0x03]), at: 0)
146+
mmap.sync()
147+
}
148+
```
149+
150+
Notes:
151+
- `offset` must be page-aligned (e.g. `getpagesize()`).
152+
- Mapping size must be greater than 0 and within file bounds.
153+
- If `size` is `nil`, the mapping size is `fileSize - offset`.
154+
- Use `.share` to write back to disk, `.private` for copy-on-write.
155+
- `MAP_SHARED` mappings can observe external process writes once they are flushed (platform behavior applies).
156+
132157
## License
133158

134159
`STFilePath` is available under the MIT license. See the [LICENSE](LICENSE) file for more info.

README_zh-CN.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,31 @@ Task {
129129
}
130130
```
131131

132+
### 内存映射(mmap)
133+
134+
`STFile` 支持作用域内存映射,便于快速读写。
135+
136+
```swift
137+
import STFilePath
138+
139+
let file = STFile("path/to/data.bin")
140+
try file.setSize(4096)
141+
142+
try file.withMmap { mmap in
143+
let data = mmap.read()
144+
print("bytes:", data.count)
145+
try mmap.write(Data([0x01, 0x02, 0x03]), at: 0)
146+
mmap.sync()
147+
}
148+
```
149+
150+
注意事项:
151+
- `offset` 必须按页大小对齐(例如 `getpagesize()`)。
152+
- 映射大小必须大于 0 且不能超出文件范围。
153+
-`size``nil` 时,映射大小为 `fileSize - offset`
154+
- `.share` 会写回磁盘,`.private` 为写时复制(不回写)。
155+
- `MAP_SHARED` 的映射在外部进程写入并刷新后可被观察到(具体行为以平台为准)。
156+
132157
## 许可证
133158

134159
`STFilePath` 在 MIT 许可下可用。有关更多信息,请参阅 [LICENSE](LICENSE) 文件。

Sources/STFilePath/STFile+MMAP.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,26 @@ public class STFileMMAP {
116116
size: Int?,
117117
offset: Int) throws {
118118

119-
guard file.isExist else {
119+
guard file.isExists else {
120120
throw STPathError(message: "[en] Cannot open non-existent file at \'\(file.url.path)\' [zh] 无法打开不存在的文件 \'\(file.url.path)\'")
121121
}
122-
123-
self.descriptor = try file.system.open(flag1: .readAndWrite, flag2: nil, mode: nil)
122+
123+
let openType: STFileSystem.OpenType = prot.contains(.write) ? .readAndWrite : .readOnly
124+
self.descriptor = try file.system.open(flag1: openType, flag2: nil, mode: nil)
124125

125126
do {
126127
let info = try file.system.stat(descriptor: descriptor)
127-
let fileSize = info.st_size
128-
let mapSize = size ?? Int(fileSize)
128+
let fileSize = Int(info.st_size)
129+
let mapSize = size ?? (fileSize - offset)
130+
131+
guard offset >= 0 else {
132+
throw STPathError(message: "[en] Offset must be greater than or equal to 0 [zh] 偏移量必须大于或等于 0")
133+
}
134+
135+
let pageSize = Int(getpagesize())
136+
if offset % pageSize != 0 {
137+
throw STPathError(message: "[en] Offset must be page-aligned (\(pageSize)) [zh] 偏移量必须按页大小对齐 (\(pageSize))")
138+
}
129139

130140
guard mapSize > 0 else {
131141
throw STPathError(message: "[en] Mapping size must be greater than 0 [zh] 映射大小必须大于 0")
@@ -135,6 +145,10 @@ public class STFileMMAP {
135145
throw STPathError(message: "[en] Mapping size (\(mapSize)) cannot exceed file size (\(fileSize)). Use file.setSize() first. [zh] 映射大小 (\(mapSize)) 不能超过文件大小 (\(fileSize))。请先使用 file.setSize()。")
136146
}
137147

148+
if offset + mapSize > fileSize {
149+
throw STPathError(message: "[en] Mapping range exceeds file size (offset \(offset) + size \(mapSize) > \(fileSize)). [zh] 映射范围超出文件大小(偏移 \(offset) + 大小 \(mapSize) > \(fileSize))。")
150+
}
151+
138152
self.size = mapSize
139153
self.startPoint = Darwin.mmap(nil,
140154
self.size,

0 commit comments

Comments
 (0)