diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml
index 307806d..727ae07 100644
--- a/.github/workflows/pr-ci.yml
+++ b/.github/workflows/pr-ci.yml
@@ -110,13 +110,13 @@ jobs:
include:
- os: ubuntu-latest
script: bash scripts/ci/build.sh
- output: out/omnitalk
+ output: out/classicstack
- os: macos-latest
script: bash scripts/ci/build.sh
- output: out/omnitalk
+ output: out/classicstack
- os: windows-latest
script: ./scripts/ci/build.ps1
- output: out/omnitalk.exe
+ output: out/classicstack.exe
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -132,7 +132,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y libpcap-dev
- - name: Build omnitalk (Linux/macOS)
+ - name: Build classicstack (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
env:
@@ -141,7 +141,7 @@ jobs:
OUTPUT: ${{ matrix.output }}
run: ${{ matrix.script }}
- - name: Build omnitalk (Windows)
+ - name: Build classicstack (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml
index 6cdf567..5ff401c 100644
--- a/.github/workflows/release-main.yml
+++ b/.github/workflows/release-main.yml
@@ -49,57 +49,57 @@ jobs:
# Linux - all
- os: ubuntu-latest
variant: all
- artifact_name: omnitalk-linux
- archive_name: omnitalk-${{ needs.version.outputs.release_tag }}-linux-amd64.tar.gz
+ artifact_name: classicstack-linux
+ archive_name: classicstack-${{ needs.version.outputs.release_tag }}-linux-amd64.tar.gz
build_script: bash scripts/ci/build.sh
package_script: bash scripts/ci/package-release.sh
target_os: linux
- output: out/omnitalk
+ output: out/classicstack
# Linux - router
- os: ubuntu-latest
variant: router
- artifact_name: omnitalk-router-linux
- archive_name: omnitalk-router-${{ needs.version.outputs.release_tag }}-linux-amd64.tar.gz
+ artifact_name: classicstack-router-linux
+ archive_name: classicstack-router-${{ needs.version.outputs.release_tag }}-linux-amd64.tar.gz
build_script: bash scripts/ci/build.sh
package_script: bash scripts/ci/package-release.sh
target_os: linux
- output: out/omnitalk-router
+ output: out/classicstack-router
# macOS - all
- os: macos-latest
variant: all
- artifact_name: omnitalk-macos
- archive_name: omnitalk-${{ needs.version.outputs.release_tag }}-macos-amd64.zip
+ artifact_name: classicstack-macos
+ archive_name: classicstack-${{ needs.version.outputs.release_tag }}-macos-amd64.zip
build_script: bash scripts/ci/build.sh
package_script: bash scripts/ci/package-release.sh
target_os: macos
- output: out/omnitalk
+ output: out/classicstack
# macOS - router
- os: macos-latest
variant: router
- artifact_name: omnitalk-router-macos
- archive_name: omnitalk-router-${{ needs.version.outputs.release_tag }}-macos-amd64.zip
+ artifact_name: classicstack-router-macos
+ archive_name: classicstack-router-${{ needs.version.outputs.release_tag }}-macos-amd64.zip
build_script: bash scripts/ci/build.sh
package_script: bash scripts/ci/package-release.sh
target_os: macos
- output: out/omnitalk-router
+ output: out/classicstack-router
# Windows - all
- os: windows-latest
variant: all
- artifact_name: omnitalk-windows
- archive_name: omnitalk-${{ needs.version.outputs.release_tag }}-windows-amd64.zip
+ artifact_name: classicstack-windows
+ archive_name: classicstack-${{ needs.version.outputs.release_tag }}-windows-amd64.zip
build_script: ./scripts/ci/build.ps1
package_script: ./scripts/ci/package-release.ps1
target_os: windows
- output: out/omnitalk.exe
+ output: out/classicstack.exe
# Windows - router
- os: windows-latest
variant: router
- artifact_name: omnitalk-router-windows
- archive_name: omnitalk-router-${{ needs.version.outputs.release_tag }}-windows-amd64.zip
+ artifact_name: classicstack-router-windows
+ archive_name: classicstack-router-${{ needs.version.outputs.release_tag }}-windows-amd64.zip
build_script: ./scripts/ci/build.ps1
package_script: ./scripts/ci/package-release.ps1
target_os: windows
- output: out/omnitalk-router.exe
+ output: out/classicstack-router.exe
steps:
- name: Checkout
uses: actions/checkout@v4
diff --git a/.gitignore b/.gitignore
index 88c9d22..f0a8c74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,8 +35,8 @@ go.work.sum
/leases.txt
# Generated by scripts/ci/build.ps1
-/cmd/omnitalk/resource.syso
-/cmd/omnitalk/versioninfo.json
+/cmd/classicstack/resource.syso
+/cmd/classicstack/versioninfo.json
._htmlcache/
.macgarden/
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 9c13d55..d73e15f 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -1,6 +1,6 @@
-# OmniTalk Architecture
+# ClassicStack Architecture
-OmniTalk is a Go AppleTalk Phase 2 router and AFP file server. It bridges
+ClassicStack is a Go AppleTalk Phase 2 router and AFP file server. It bridges
legacy Apple networking protocols to modern environments — EtherTalk
(raw Ethernet), LToUDP (multicast UDP), TashTalk (serial), and
virtual LocalTalk transports — and serves AFP volumes over both the
@@ -13,7 +13,7 @@ import.
## Module map
```
-cmd/omnitalk/ wiring only — flag/INI parsing, service registration
+cmd/classicstack/ wiring only — flag/INI parsing, service registration
config/ single typed config tree; INI loader, validation
protocol/ wire format only (codec + constants, zero I/O)
ddp/ DDP datagram + MacRoman codec
@@ -59,11 +59,11 @@ cmd → service → (protocol | port | pkg)
not about higher protocols.
- `service/*` owns sockets, sessions, and state machines. It composes
`protocol` codecs over `port` transports.
-- `pkg/*` is reusable outside OmniTalk. It must not import anything
+- `pkg/*` is reusable outside ClassicStack. It must not import anything
under `service/`, `port/`, `cmd/`, or `router/`.
-- `internal/*` is private to OmniTalk. Mocks and shared test harness
+- `internal/*` is private to ClassicStack. Mocks and shared test harness
live here.
-- `cmd/omnitalk/` does no business logic. It parses configuration
+- `cmd/classicstack/` does no business logic. It parses configuration
and wires services together.
## Core interfaces
@@ -86,7 +86,7 @@ Single typed tree in `config/`. Two loaders feed it:
1. TOML — `config.Load(path)` parses `server.toml` via `knadh/koanf`
with the `pelletier/go-toml` v2 parser.
-2. Flags — `cmd/omnitalk/main.go` overlays CLI flags on top of the
+2. Flags — `cmd/classicstack/main.go` overlays CLI flags on top of the
file defaults.
`config.Root.Validate()` runs once before services start. Services
@@ -95,14 +95,14 @@ are immutable: ports do not mutate themselves after `Start()`.
## Logging and telemetry
-OmniTalk has two logging packages with distinct jobs:
+ClassicStack has two logging packages with distinct jobs:
- **`netlog/`** is the call-site API. Services and ports use
`netlog.Debug`, `netlog.Info`, `netlog.Warn`. The facade keeps call
sites short (no per-package `*slog.Logger` plumbing) while still
- routing through whatever structured handler `cmd/omnitalk` installs.
+ routing through whatever structured handler `cmd/classicstack` installs.
- **`pkg/logging/`** is the slog factory used once at startup.
- `cmd/omnitalk` calls `logging.New("OmniTalk", ...)` to build a
+ `cmd/classicstack` calls `logging.New("ClassicStack", ...)` to build a
`*slog.Logger` with the configured handler (console, JSON, or both)
and installs it via `netlog.SetLogger`. Use this directly only when
you need a `*slog.Logger` value — e.g. attaching structured fields
@@ -114,14 +114,14 @@ format, and the slog handler stamps every record with a `source`
attribute that JSON consumers can filter on.
Stdlib `log.Printf` and `log.Fatal` are not used inside library code.
-`cmd/omnitalk/main.go` uses `log.Fatal*` only for unrecoverable startup
+`cmd/classicstack/main.go` uses `log.Fatal*` only for unrecoverable startup
errors before any logger is wired.
Telemetry is `pkg/telemetry`, separate from logs. Default backend is
`expvar` (stdlib, zero deps). Initial counters:
-- `omnitalk_router_frames_in_total`
-- `omnitalk_afp_commands_total`
-- `omnitalk_aarp_probe_retries_total`
+- `classicstack_router_frames_in_total`
+- `classicstack_afp_commands_total`
+- `classicstack_aarp_probe_retries_total`
A future `//go:build otel` file will swap in an OpenTelemetry backend
without touching call sites.
@@ -150,7 +150,7 @@ proceeds one type per commit with golden hex round-trip tests.
## Timer and retry patterns
-OmniTalk does not use exponential backoff. The protocols predate it.
+ClassicStack does not use exponential backoff. The protocols predate it.
Three canonical shapes:
1. **Reliable-delivery retransmits** (ATP-style). Per-transaction
diff --git a/CLAUDE.md b/CLAUDE.md
index 21f8b17..a68af64 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,16 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-OmniTalk is a Go-based AppleTalk Phase 2 router and AFP file server. It bridges legacy Apple networking protocols to modern environments, supporting EtherTalk (raw Ethernet), LToUDP (multicast UDP), TashTalk (serial), and virtual LocalTalk transports.
+ClassicStack is a Go-based AppleTalk Phase 2 router and AFP file server. It bridges legacy Apple networking protocols to modern environments, supporting EtherTalk (raw Ethernet), LToUDP (multicast UDP), TashTalk (serial), and virtual LocalTalk transports.
-**Module:** `github.com/pgodw/omnitalk`
+**Module:** `github.com/ObsoleteMadness/ClassicStack`
**Go version:** 1.23.0
## Commands
```bash
# Build
-go build -o omnitalk ./cmd/omnitalk
+go build -o classicstack ./cmd/classicstack
# Run all tests
go test ./...
@@ -22,10 +22,10 @@ go test ./...
go test ./service/afp/...
# Run with TOML config
-./omnitalk # auto-loads server.toml if present
+./classicstack # auto-loads server.toml if present
# Run with flags (see README.md for full list)
-./omnitalk -ethertalk eth0 -zone "MyZone"
+./classicstack -ethertalk eth0 -zone "MyZone"
```
## Architecture
@@ -33,10 +33,10 @@ go test ./service/afp/...
### Core Data Flow
```
-cmd/omnitalk/main.go → Ports → Router → Services
+cmd/classicstack/main.go → Ports → Router → Services
```
-1. **Entry point** (`cmd/omnitalk/`) parses CLI flags and `server.toml`, constructs ports, wires them to the router, and starts services.
+1. **Entry point** (`cmd/classicstack/`) parses CLI flags and `server.toml`, constructs ports, wires them to the router, and starts services.
2. **Router** (`router/`) receives DDP datagrams from all ports, maintains the `RoutingTable` and `ZoneInformationTable`, and dispatches to services by socket number or forwards to other ports.
3. **Ports** (`port/`) abstract network interfaces. All implement `port.Port` (Unicast/Broadcast/Multicast). Implementations: `ethertalk`, `localtalk/ltoudp`, `localtalk/tashtalk`, `localtalk/virtual`.
4. **Services** (`service/`) plug into the router by registering socket numbers. Each implements `service.Service`.
diff --git a/Makefile b/Makefile
index 10ce0be..1836b16 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ TAGS ?= all
.PHONY: build test test-race test-tags lint vuln gosec fuzz clean
build:
- go build -tags "$(TAGS)" -o omnitalk ./cmd/omnitalk
+ go build -tags "$(TAGS)" -o classicstack ./cmd/classicstack
test:
go test -tags "$(TAGS)" ./...
@@ -30,5 +30,5 @@ fuzz:
done
clean:
- rm -f omnitalk omnitalk.exe
+ rm -f classicstack classicstack.exe
rm -rf out dist
diff --git a/README.md b/README.md
index e250338..9859074 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-

+

-# OmniTalk
+# ClassicStack
-### OmniTalk is a all-in-one AppleTalk Phase 2 router, MacIP Router and AFP file server for bridging classic Apple networking into modern environments. 🍏💾
+### ClassicStack is a all-in-one AppleTalk Phase 2 router, MacIP Router and AFP file server for bridging classic Apple networking into modern environments. 🍏💾
@@ -30,23 +30,23 @@ core interfaces, logging/telemetry, and the AFP design — see
## Quick start
- Copy server.toml.example to server.toml and edit values.
-- Run OmniTalk with no flags to auto-load server.toml.
+- Run ClassicStack with no flags to auto-load server.toml.
- Or pass a config file explicitly with -config.
Examples:
~~~bash
-./omnitalk -config server.toml
+./classicstack -config server.toml
~~~
~~~powershell
-.\omnitalk.exe -config server.toml
+.\classicstack.exe -config server.toml
~~~
Config-loading rule:
- -config cannot be combined with other flags.
-- If no flags are supplied, OmniTalk auto-loads server.toml if present.
+- If no flags are supplied, ClassicStack auto-loads server.toml if present.
---
@@ -65,13 +65,13 @@ Requirements:
Build from repository root:
~~~bash
-go build ./cmd/omnitalk
+go build ./cmd/classicstack
~~~
Build with explicit binary name:
~~~bash
-go build -o omnitalk ./cmd/omnitalk
+go build -o classicstack ./cmd/classicstack
~~~
Build with explicit semantic version metadata:
@@ -79,7 +79,7 @@ Build with explicit semantic version metadata:
~~~bash
go build -trimpath \
-ldflags "-X main.BuildVersion=1.2.3 -X main.BuildCommit=$(git rev-parse --short HEAD) -X main.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
- -o omnitalk ./cmd/omnitalk
+ -o classicstack ./cmd/classicstack
~~~
Build using the shared local/CI scripts:
@@ -97,7 +97,7 @@ bash scripts/ci/test.sh
Print runtime/build version info:
~~~bash
-./omnitalk -version
+./classicstack -version
~~~
Run tests:
@@ -114,8 +114,8 @@ go test ./...
- GitHub Actions calls the same scripts under `scripts/ci/` that you can run locally.
- Release assets are produced for Linux, macOS, and Windows.
- Release packages include the repository `dist/` content.
-- Windows release binaries include icon and file version metadata from `icons/omnitalk.ico`.
-- macOS release bundles include app icon metadata from `icons/omnitalk.icns`.
+- Windows release binaries include icon and file version metadata from `icons/classicstack.ico`.
+- macOS release bundles include app icon metadata from `icons/classicstack.icns`.
- Go build/test already ignores non-Go folders; additionally `scripts/ci/test.sh` and `scripts/ci/test.ps1` explicitly exclude `dist`, `icon`, and `icons` from the package list.
## Status and provenance
@@ -140,7 +140,7 @@ Route AppleTalk between EtherTalk and LocalTalk ports, with RTMP/ZIP/NBP service
Use the built-in pcap listing mode:
~~~powershell
-.\omnitalk.exe -list-pcap-devices
+.\classicstack.exe -list-pcap-devices
~~~
This prints available interface names and pcap device IDs. Use the device string in [EtherTalk] device, for example:
@@ -248,14 +248,14 @@ Why `wifi` mode exists:
- Many Wi-Fi adapters and AP paths reject or rewrite frames when the source MAC does not match the host adapter MAC.
- On Windows, the miniport/NDIS path commonly drops transmit frames when source hardware address does not match the host adapter MAC.
-- In `wifi` mode OmniTalk rewrites outbound EtherTalk frame source MAC to the host adapter MAC and updates AARP hardware fields accordingly.
-- For inbound traffic, OmniTalk reverses destination rewrite using a short-lived peer-to-virtual mapping so the EtherTalk port still sees the expected virtual MAC identity.
+- In `wifi` mode ClassicStack rewrites outbound EtherTalk frame source MAC to the host adapter MAC and updates AARP hardware fields accordingly.
+- For inbound traffic, ClassicStack reverses destination rewrite using a short-lived peer-to-virtual mapping so the EtherTalk port still sees the expected virtual MAC identity.
- This is effectively an L2 NAT-style shim for MAC identities (not MacIP IP-layer NAT).
Recommended settings:
- On Wi-Fi, set `bridge_mode=wifi` (or leave `auto` and verify it detected Wi-Fi correctly).
-- Set `bridge_host_mac` to your actual Wi-Fi adapter MAC when needed; if blank, OmniTalk falls back to `hw_address`.
+- Set `bridge_host_mac` to your actual Wi-Fi adapter MAC when needed; if blank, ClassicStack falls back to `hw_address`.
- On wired Ethernet, prefer `bridge_mode=ethernet` or `auto`.
##### Wi-Fi troubleshooting
@@ -264,7 +264,7 @@ Common symptoms:
- You see AppleTalk traffic in one direction only.
- AARP appears unanswered even when peers are present.
-- OmniTalk works on wired Ethernet but fails on the same host over Wi-Fi.
+- ClassicStack works on wired Ethernet but fails on the same host over Wi-Fi.
Checks and fixes:
@@ -315,7 +315,7 @@ lease_file = "leases.txt"
## AFP
-OmniTalk includes an AFP file server focused on AFP 2.0-level behavior, with selective AFP 2.1/2.2 calls, exposed over both classic AppleTalk transport and modern TCP transport:
+ClassicStack includes an AFP file server focused on AFP 2.0-level behavior, with selective AFP 2.1/2.2 calls, exposed over both classic AppleTalk transport and modern TCP transport:
- DDP stack: DDP -> ATP -> ASP -> AFP
- TCP stack: TCP -> DSI -> AFP
@@ -466,8 +466,8 @@ Volume naming:
#### Netatalk compatibility
- Compatible formats: Netatalk-style extension map syntax and AppleDouble modern/legacy sidecar layouts.
-- Known differences: CNID database implementation is OmniTalk-specific (sqlite or memory), not a drop-in Netatalk CNID store.
-- OmniTalk does not currently provide a Netatalk-style extended-attribute metadata backend.
+- Known differences: CNID database implementation is ClassicStack-specific (sqlite or memory), not a drop-in Netatalk CNID store.
+- ClassicStack does not currently provide a Netatalk-style extended-attribute metadata backend.
- AFP feature coverage is practical but incomplete (for example catalog search is currently implemented as name-based search and backend-dependent).
### [Logging]
@@ -493,7 +493,7 @@ Use server.toml for repeatable deployments; use flags for quick experiments.
## Rough project layout
-- cmd/omnitalk: entrypoint, flag handling, TOML config loading, runtime wiring.
+- cmd/classicstack: entrypoint, flag handling, TOML config loading, runtime wiring.
- router: datagram dispatch, routing table, zone information table.
- port: transport implementations (EtherTalk, LocalTalk variants, rawlink, NAT helpers).
- service: protocol/application services (AEP, RTMP, ZIP, ASP/ATP/DSI, AFP, MacIP, LLAP).
diff --git a/cmd/omnitalk/afp_disabled.go b/cmd/classicstack/afp_disabled.go
similarity index 89%
rename from cmd/omnitalk/afp_disabled.go
rename to cmd/classicstack/afp_disabled.go
index cd74146..d61d95e 100644
--- a/cmd/omnitalk/afp_disabled.go
+++ b/cmd/classicstack/afp_disabled.go
@@ -3,8 +3,8 @@
package main
import (
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type afpHookDisabled struct{}
diff --git a/cmd/omnitalk/afp_enabled.go b/cmd/classicstack/afp_enabled.go
similarity index 93%
rename from cmd/omnitalk/afp_enabled.go
rename to cmd/classicstack/afp_enabled.go
index d4268a9..3d13902 100644
--- a/cmd/omnitalk/afp_enabled.go
+++ b/cmd/classicstack/afp_enabled.go
@@ -7,12 +7,12 @@ import (
"path/filepath"
"strings"
- "github.com/pgodw/omnitalk/config"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/afp"
- "github.com/pgodw/omnitalk/service/asp"
- "github.com/pgodw/omnitalk/service/dsi"
+ "github.com/ObsoleteMadness/ClassicStack/config"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
+ "github.com/ObsoleteMadness/ClassicStack/service/asp"
+ "github.com/ObsoleteMadness/ClassicStack/service/dsi"
)
type afpHookEnabled struct {
diff --git a/cmd/omnitalk/afp_hook.go b/cmd/classicstack/afp_hook.go
similarity index 91%
rename from cmd/omnitalk/afp_hook.go
rename to cmd/classicstack/afp_hook.go
index bbbc551..bea9a1c 100644
--- a/cmd/omnitalk/afp_hook.go
+++ b/cmd/classicstack/afp_hook.go
@@ -1,9 +1,9 @@
package main
import (
- "github.com/pgodw/omnitalk/config"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/config"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
// AFPHook is the cmd-layer abstraction over the optional AFP file
diff --git a/cmd/omnitalk/config_afp_test.go b/cmd/classicstack/config_afp_test.go
similarity index 97%
rename from cmd/omnitalk/config_afp_test.go
rename to cmd/classicstack/config_afp_test.go
index 081ec71..704721f 100644
--- a/cmd/omnitalk/config_afp_test.go
+++ b/cmd/classicstack/config_afp_test.go
@@ -7,8 +7,8 @@ import (
"path/filepath"
"testing"
- "github.com/pgodw/omnitalk/config"
- "github.com/pgodw/omnitalk/service/afp"
+ "github.com/ObsoleteMadness/ClassicStack/config"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
)
// loadAFPForTest is a small helper that mirrors what wireAFP does on
@@ -32,7 +32,7 @@ func TestLoadAFPConfig_VolumesAndExtensionMap(t *testing.T) {
cfgPath := filepath.Join(dir, "server.toml")
content := `[AFP]
enabled = true
-name = "OmniTalk"
+name = "ClassicStack"
zone = "EtherTalk Network"
protocols = "ddp,tcp"
binding = ":548"
diff --git a/cmd/omnitalk/config_flags.go b/cmd/classicstack/config_flags.go
similarity index 95%
rename from cmd/omnitalk/config_flags.go
rename to cmd/classicstack/config_flags.go
index 329faa3..113cac1 100644
--- a/cmd/omnitalk/config_flags.go
+++ b/cmd/classicstack/config_flags.go
@@ -1,8 +1,8 @@
package main
import (
- "github.com/pgodw/omnitalk/port/ethertalk"
- "github.com/pgodw/omnitalk/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/port/ethertalk"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
)
// flagInputs collects raw values from the CLI flags. main.go derefs each
diff --git a/cmd/omnitalk/config_ini.go b/cmd/classicstack/config_ini.go
similarity index 96%
rename from cmd/omnitalk/config_ini.go
rename to cmd/classicstack/config_ini.go
index c6a6ca4..10f7afa 100644
--- a/cmd/omnitalk/config_ini.go
+++ b/cmd/classicstack/config_ini.go
@@ -6,9 +6,9 @@ import (
"github.com/knadh/koanf/v2"
- "github.com/pgodw/omnitalk/config"
- "github.com/pgodw/omnitalk/port/ethertalk"
- "github.com/pgodw/omnitalk/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/config"
+ "github.com/ObsoleteMadness/ClassicStack/port/ethertalk"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
)
// appConfig is the cmd-local view of resolved configuration. Each
diff --git a/cmd/omnitalk/config_test.go b/cmd/classicstack/config_test.go
similarity index 100%
rename from cmd/omnitalk/config_test.go
rename to cmd/classicstack/config_test.go
diff --git a/cmd/omnitalk/doc.go b/cmd/classicstack/doc.go
similarity index 87%
rename from cmd/omnitalk/doc.go
rename to cmd/classicstack/doc.go
index 65847c1..9dcd51b 100644
--- a/cmd/omnitalk/doc.go
+++ b/cmd/classicstack/doc.go
@@ -1,5 +1,5 @@
/*
-Command omnitalk is the AppleTalk Phase 2 router and AFP file server.
+Command classicstack is the AppleTalk Phase 2 router and AFP file server.
It wires ports (EtherTalk, LToUDP, TashTalk, virtual LocalTalk) to a
router, registers the requested services (RTMP, ZIP, NBP, AEP, AFP over
diff --git a/cmd/omnitalk/extension_map.go b/cmd/classicstack/extension_map.go
similarity index 95%
rename from cmd/omnitalk/extension_map.go
rename to cmd/classicstack/extension_map.go
index bded9ae..f04a7d4 100644
--- a/cmd/omnitalk/extension_map.go
+++ b/cmd/classicstack/extension_map.go
@@ -8,7 +8,7 @@ import (
"regexp"
"strings"
- "github.com/pgodw/omnitalk/service/afp"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
)
var extMapLinePattern = regexp.MustCompile(`^(\S+)\s+"([^"]*)"\s+"([^"]*)"`)
diff --git a/cmd/omnitalk/extension_map_test.go b/cmd/classicstack/extension_map_test.go
similarity index 100%
rename from cmd/omnitalk/extension_map_test.go
rename to cmd/classicstack/extension_map_test.go
diff --git a/cmd/classicstack/macgarden_register.go b/cmd/classicstack/macgarden_register.go
new file mode 100644
index 0000000..4a03def
--- /dev/null
+++ b/cmd/classicstack/macgarden_register.go
@@ -0,0 +1,5 @@
+//go:build (afp && macgarden) || all
+
+package main
+
+import _ "github.com/ObsoleteMadness/ClassicStack/service/afpfs/macgarden"
diff --git a/cmd/omnitalk/macip_disabled.go b/cmd/classicstack/macip_disabled.go
similarity index 88%
rename from cmd/omnitalk/macip_disabled.go
rename to cmd/classicstack/macip_disabled.go
index 29a11d6..81c75fc 100644
--- a/cmd/omnitalk/macip_disabled.go
+++ b/cmd/classicstack/macip_disabled.go
@@ -2,7 +2,7 @@
package main
-import "github.com/pgodw/omnitalk/netlog"
+import "github.com/ObsoleteMadness/ClassicStack/netlog"
// wireMacIP is the no-op stub used when the binary is built without the
// macip tag. It logs a warning if the operator asked for MacIP and exits
diff --git a/cmd/omnitalk/macip_enabled.go b/cmd/classicstack/macip_enabled.go
similarity index 94%
rename from cmd/omnitalk/macip_enabled.go
rename to cmd/classicstack/macip_enabled.go
index c253295..d948ef0 100644
--- a/cmd/omnitalk/macip_enabled.go
+++ b/cmd/classicstack/macip_enabled.go
@@ -7,11 +7,11 @@ import (
"net"
"strings"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/hwaddr"
- "github.com/pgodw/omnitalk/port/rawlink"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/macip"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/hwaddr"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/macip"
)
type macipHook struct {
diff --git a/cmd/omnitalk/macip_hook.go b/cmd/classicstack/macip_hook.go
similarity index 93%
rename from cmd/omnitalk/macip_hook.go
rename to cmd/classicstack/macip_hook.go
index f1c2144..adcbbe9 100644
--- a/cmd/omnitalk/macip_hook.go
+++ b/cmd/classicstack/macip_hook.go
@@ -1,8 +1,8 @@
package main
import (
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
// MacIPHook is the cmd-layer abstraction over the optional MacIP gateway.
diff --git a/cmd/omnitalk/macip_test.go b/cmd/classicstack/macip_test.go
similarity index 100%
rename from cmd/omnitalk/macip_test.go
rename to cmd/classicstack/macip_test.go
diff --git a/cmd/omnitalk/main.go b/cmd/classicstack/main.go
similarity index 94%
rename from cmd/omnitalk/main.go
rename to cmd/classicstack/main.go
index cdc80f6..3fa59cb 100644
--- a/cmd/omnitalk/main.go
+++ b/cmd/classicstack/main.go
@@ -12,27 +12,27 @@ import (
"strings"
"syscall"
- "github.com/pgodw/omnitalk/config"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/hwaddr"
- "github.com/pgodw/omnitalk/pkg/logging"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/port/ethertalk"
- "github.com/pgodw/omnitalk/port/localtalk"
- "github.com/pgodw/omnitalk/port/rawlink"
- "github.com/pgodw/omnitalk/router"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/aep"
- "github.com/pgodw/omnitalk/service/llap"
- "github.com/pgodw/omnitalk/service/rtmp"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/config"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/hwaddr"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/logging"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/port/ethertalk"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/router"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/aep"
+ "github.com/ObsoleteMadness/ClassicStack/service/llap"
+ "github.com/ObsoleteMadness/ClassicStack/service/rtmp"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
configPath := flag.String("config", "", "Path to TOML config file (cannot be combined with other flags)")
- showVersion := flag.Bool("version", false, "Print OmniTalk version information and exit")
+ showVersion := flag.Bool("version", false, "Print ClassicStack version information and exit")
logLevel := flag.String("log-level", "info", "Minimum log level: debug, info, warn")
logTraffic := flag.Bool("log-traffic", false, "Log network traffic at debug level (requires -log-level debug)")
@@ -90,7 +90,7 @@ func main() {
flag.Parse()
if *showVersion {
- fmt.Printf("omnitalk %s\n", BuildVersion)
+ fmt.Printf("classicstack %s\n", BuildVersion)
fmt.Printf("commit: %s\n", BuildCommit)
fmt.Printf("built: %s\n", BuildDate)
fmt.Printf("go: %s\n", runtime.Version())
@@ -178,7 +178,7 @@ func main() {
// attributes. Each service will eventually take a *slog.Logger
// directly; until then, netlog.* calls forward here.
slogLevel, _ := logging.ParseLevel(cfg.LogLevel)
- rootLogger := logging.New("OmniTalk", logging.Options{
+ rootLogger := logging.New("ClassicStack", logging.Options{
Sinks: []logging.Sink{{Writer: os.Stderr, Format: logging.FormatConsole, Level: slogLevel}},
})
logging.SetDefault(rootLogger)
diff --git a/cmd/omnitalk/main_macip_test.go b/cmd/classicstack/main_macip_test.go
similarity index 100%
rename from cmd/omnitalk/main_macip_test.go
rename to cmd/classicstack/main_macip_test.go
diff --git a/cmd/omnitalk/packetdump.go b/cmd/classicstack/packetdump.go
similarity index 95%
rename from cmd/omnitalk/packetdump.go
rename to cmd/classicstack/packetdump.go
index df1e1b5..8f26be4 100644
--- a/cmd/omnitalk/packetdump.go
+++ b/cmd/classicstack/packetdump.go
@@ -6,7 +6,7 @@ import (
"log"
"os"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// PacketDumper is a generic sink used by services to emit parsed packet logs.
diff --git a/cmd/omnitalk/version.go b/cmd/classicstack/version.go
similarity index 100%
rename from cmd/omnitalk/version.go
rename to cmd/classicstack/version.go
diff --git a/cmd/omnitalk/macgarden_register.go b/cmd/omnitalk/macgarden_register.go
deleted file mode 100644
index 061765b..0000000
--- a/cmd/omnitalk/macgarden_register.go
+++ /dev/null
@@ -1,5 +0,0 @@
-//go:build (afp && macgarden) || all
-
-package main
-
-import _ "github.com/pgodw/omnitalk/service/afpfs/macgarden"
diff --git a/config/config.go b/config/config.go
index 9f75994..abe607a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,10 +1,10 @@
-// Package config abstracts where OmniTalk's configuration comes from
+// Package config abstracts where ClassicStack's configuration comes from
// (TOML file today; environment variables, JSON, etc. tomorrow). It owns
// no schema knowledge: each component decides what keys it consumes by
// reading from the returned koanf instance.
//
// Defaults live with the consumers (typically as flag defaults in
-// cmd/omnitalk). The config package's only job is to surface a populated
+// cmd/classicstack). The config package's only job is to surface a populated
// koanf source to those consumers.
package config
diff --git a/config/loadtoml_test.go b/config/loadtoml_test.go
index ffce6e8..f2f12e0 100644
--- a/config/loadtoml_test.go
+++ b/config/loadtoml_test.go
@@ -10,8 +10,8 @@ func TestLoad_ExampleFile(t *testing.T) {
if err != nil {
t.Fatalf("Load(server.toml.example): %v", err)
}
- if got := src.K.String("AFP.name"); got != "OmniTalk" {
- t.Fatalf("AFP.name = %q, want %q", got, "OmniTalk")
+ if got := src.K.String("AFP.name"); got != "ClassicStack" {
+ t.Fatalf("AFP.name = %q, want %q", got, "ClassicStack")
}
if vols := src.K.MapKeys("AFP.Volumes"); len(vols) != 2 {
t.Fatalf("AFP.Volumes = %d, want 2", len(vols))
diff --git a/dist/Shared/welcome.txt b/dist/Shared/welcome.txt
index 7c04d69..a9e87f3 100644
--- a/dist/Shared/welcome.txt
+++ b/dist/Shared/welcome.txt
@@ -1,3 +1,3 @@
-Welcome to OmniTalk.
+Welcome to ClassicStack.
This volume is configured to be read/write.
\ No newline at end of file
diff --git a/dist/server.toml b/dist/server.toml
index b221dc0..b5b2340 100644
--- a/dist/server.toml
+++ b/dist/server.toml
@@ -35,7 +35,7 @@ nameserver = "1.1.1.1"
[AFP]
enabled = true
-name = "OmniTalk"
+name = "ClassicStack"
zone = "EtherTalk Network"
protocols = "ddp,tcp"
binding = ":548"
diff --git a/go.mod b/go.mod
index 5c63d15..31b40ba 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/pgodw/omnitalk
+module github.com/ObsoleteMadness/ClassicStack
go 1.23.0
diff --git a/internal/testutil/mock_port.go b/internal/testutil/mock_port.go
index dba2b4b..dca47f3 100644
--- a/internal/testutil/mock_port.go
+++ b/internal/testutil/mock_port.go
@@ -1,11 +1,11 @@
-// Package testutil provides shared test helpers used across OmniTalk's
+// Package testutil provides shared test helpers used across ClassicStack's
// service and port packages. Live under internal/ so external consumers
// cannot depend on these mocks; only project tests may import.
package testutil
import (
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
)
// MockPort is a fake port.Port whose behaviour is driven by func fields.
diff --git a/internal/testutil/mock_router.go b/internal/testutil/mock_router.go
index dac9925..e59f579 100644
--- a/internal/testutil/mock_router.go
+++ b/internal/testutil/mock_router.go
@@ -1,9 +1,9 @@
package testutil
import (
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/protocol/ddp"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// MockRouter is a fake service.Router whose behaviour is driven by func
diff --git a/netlog/netlog.go b/netlog/netlog.go
index a8b7261..7a41537 100644
--- a/netlog/netlog.go
+++ b/netlog/netlog.go
@@ -1,6 +1,6 @@
-// Package netlog is OmniTalk's logging API.
+// Package netlog is ClassicStack's logging API.
//
-// It is a thin facade over log/slog: cmd/omnitalk constructs a structured
+// It is a thin facade over log/slog: cmd/classicstack constructs a structured
// logger via pkg/logging and installs it here with SetLogger, then every
// service calls Debug/Info/Warn from this package. The facade keeps call
// sites short (no per-package logger plumbing) while still letting the
@@ -20,7 +20,7 @@ import (
"strings"
"sync"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
)
// Level mirrors the legacy three-value enum but maps onto slog.Level.
diff --git a/omnitalk b/omnitalk
index 2859c97..296801c 100644
Binary files a/omnitalk and b/omnitalk differ
diff --git a/pkg/hwaddr/hwaddr.go b/pkg/hwaddr/hwaddr.go
index ce844c6..f608e97 100644
--- a/pkg/hwaddr/hwaddr.go
+++ b/pkg/hwaddr/hwaddr.go
@@ -1,7 +1,7 @@
// Package hwaddr provides unified hardware-address types covering Ethernet
// (EUI-48), LocalTalk (8-bit LLAP node ID), and AppleTalk (24-bit DDP
// address), plus parsing, formatting, generation, and conversion between
-// them. It replaces ad-hoc helpers previously scattered across cmd/omnitalk,
+// them. It replaces ad-hoc helpers previously scattered across cmd/classicstack,
// port/ethertalk, port/localtalk, and service/macip.
package hwaddr
@@ -31,7 +31,7 @@ type AppleTalk struct {
// synthesising Ethernet addresses from AppleTalk addresses.
var AppleOUI = [3]byte{0x00, 0x00, 0x07}
-// MacIPOUI is the locally administered prefix historically used by OmniTalk's
+// MacIPOUI is the locally administered prefix historically used by ClassicStack's
// MacIP gateway to fabricate per-node MACs for DHCP. Bit 1 of the first octet
// is set, marking the address as locally administered.
var MacIPOUI = [3]byte{0x02, 0x00, 0x00}
@@ -188,7 +188,7 @@ func AppleTalkFromEthernet(oui [3]byte, e Ethernet) (AppleTalk, bool) {
// Layout: 0x02 (locally administered) | netHi | netLo | node | 'M' | 'I'.
// The suffix "MI" distinguishes these addresses from generic AARP-style
// syntheses and preserves wire-level compatibility with existing DHCP
-// leases issued against OmniTalk MacIP.
+// leases issued against ClassicStack MacIP.
func MacIPEthernetFromAppleTalk(a AppleTalk) Ethernet {
return Ethernet{0x02, byte(a.Network >> 8), byte(a.Network), a.Node, 'M', 'I'}
}
diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go
index 260e198..e06edfe 100644
--- a/pkg/telemetry/telemetry.go
+++ b/pkg/telemetry/telemetry.go
@@ -1,4 +1,4 @@
-// Package telemetry is OmniTalk's metrics abstraction. It exposes
+// Package telemetry is ClassicStack's metrics abstraction. It exposes
// Counter, Gauge, and Histogram types with a default expvar-backed
// implementation that ships as part of the stdlib and requires no
// extra dependencies. A build-tagged OpenTelemetry backend may be
@@ -10,13 +10,13 @@
//
// Usage:
//
-// var framesIn = telemetry.NewCounter("omnitalk_router_frames_in_total")
+// var framesIn = telemetry.NewCounter("classicstack_router_frames_in_total")
// framesIn.Inc()
// framesIn.Add(n)
//
// Metric names follow Prometheus-style lower_snake_case with a unit
// suffix (_total, _seconds, _bytes). Labels are encoded into the name
-// for the expvar backend (e.g. "omnitalk_afp_commands_total_OpenFork")
+// for the expvar backend (e.g. "classicstack_afp_commands_total_OpenFork")
// because expvar does not support label dimensions natively; the OTel
// backend splits them back out.
package telemetry
@@ -88,17 +88,17 @@ func NewHistogram(name string) Histogram {
type expvarCounter struct{ n atomic.Int64 }
-func (c *expvarCounter) Inc() { c.n.Add(1) }
-func (c *expvarCounter) Add(d int64) { c.n.Add(d) }
-func (c *expvarCounter) Value() int64 { return c.n.Load() }
-func (c *expvarCounter) String() string { return i64string(c.n.Load()) }
+func (c *expvarCounter) Inc() { c.n.Add(1) }
+func (c *expvarCounter) Add(d int64) { c.n.Add(d) }
+func (c *expvarCounter) Value() int64 { return c.n.Load() }
+func (c *expvarCounter) String() string { return i64string(c.n.Load()) }
type expvarGauge struct{ n atomic.Int64 }
-func (g *expvarGauge) Set(v int64) { g.n.Store(v) }
-func (g *expvarGauge) Add(d int64) { g.n.Add(d) }
-func (g *expvarGauge) Value() int64 { return g.n.Load() }
-func (g *expvarGauge) String() string { return i64string(g.n.Load()) }
+func (g *expvarGauge) Set(v int64) { g.n.Store(v) }
+func (g *expvarGauge) Add(d int64) { g.n.Add(d) }
+func (g *expvarGauge) Value() int64 { return g.n.Load() }
+func (g *expvarGauge) String() string { return i64string(g.n.Load()) }
type expvarHistogram struct {
count atomic.Int64
diff --git a/port/ethertalk/doc.go b/port/ethertalk/doc.go
index bf95b21..fbcdac5 100644
--- a/port/ethertalk/doc.go
+++ b/port/ethertalk/doc.go
@@ -1,5 +1,5 @@
// Package ethertalk implements EtherTalk (AppleTalk Phase 2 over
-// Ethernet) as an OmniTalk port.
+// Ethernet) as an ClassicStack port.
//
// Frames are sent and received via libpcap/Npcap on the host
// interface. The package also implements AARP (RFC 1742, Appendix A)
diff --git a/port/ethertalk/ethertalk.go b/port/ethertalk/ethertalk.go
index 839150f..c0dc236 100644
--- a/port/ethertalk/ethertalk.go
+++ b/port/ethertalk/ethertalk.go
@@ -7,10 +7,10 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
var (
diff --git a/port/ethertalk/ethertalk_bridge.go b/port/ethertalk/ethertalk_bridge.go
index ede28fb..3091a17 100644
--- a/port/ethertalk/ethertalk_bridge.go
+++ b/port/ethertalk/ethertalk_bridge.go
@@ -8,7 +8,7 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
)
type bridgeMode uint8
diff --git a/port/ethertalk/ethertalk_bridge_test.go b/port/ethertalk/ethertalk_bridge_test.go
index f3440af..05c5543 100644
--- a/port/ethertalk/ethertalk_bridge_test.go
+++ b/port/ethertalk/ethertalk_bridge_test.go
@@ -5,7 +5,7 @@ import (
"encoding/binary"
"testing"
- "github.com/pgodw/omnitalk/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
)
func TestBridgeAdapterInboundPassThroughCopy(t *testing.T) {
diff --git a/port/ethertalk/metrics.go b/port/ethertalk/metrics.go
index c4c72a9..acfded1 100644
--- a/port/ethertalk/metrics.go
+++ b/port/ethertalk/metrics.go
@@ -1,5 +1,5 @@
package ethertalk
-import "github.com/pgodw/omnitalk/pkg/telemetry"
+import "github.com/ObsoleteMadness/ClassicStack/pkg/telemetry"
-var aarpProbeRetriesTotal = telemetry.NewCounter("omnitalk_aarp_probe_retries_total")
+var aarpProbeRetriesTotal = telemetry.NewCounter("classicstack_aarp_probe_retries_total")
diff --git a/port/ethertalk/pcap.go b/port/ethertalk/pcap.go
index b257dcd..63ca7ed 100644
--- a/port/ethertalk/pcap.go
+++ b/port/ethertalk/pcap.go
@@ -3,9 +3,9 @@ package ethertalk
import (
"net"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
)
// etherTalkBPFFilter selects EtherTalk Phase 2 frames carried as
diff --git a/port/ethertalk/tap.go b/port/ethertalk/tap.go
index 5e2a3e0..79330a5 100644
--- a/port/ethertalk/tap.go
+++ b/port/ethertalk/tap.go
@@ -1,6 +1,6 @@
package ethertalk
-import "github.com/pgodw/omnitalk/port/rawlink"
+import "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
// NewTapPort creates an EtherTalk port over a TAP-style raw link backend.
// TAP support depends on rawlink.OpenTAP for the current platform.
diff --git a/port/localtalk/doc.go b/port/localtalk/doc.go
index 393d32e..8d0a3b6 100644
--- a/port/localtalk/doc.go
+++ b/port/localtalk/doc.go
@@ -1,5 +1,5 @@
// Package localtalk implements LocalTalk (AppleTalk Phase 1) as an
-// OmniTalk port.
+// ClassicStack port.
//
// LLAP frames travel over one of several physical/virtual transports
// implemented in subpackages: LToUDP (UDP multicast on
diff --git a/port/localtalk/llap.go b/port/localtalk/llap.go
index bb47335..5e2eb42 100644
--- a/port/localtalk/llap.go
+++ b/port/localtalk/llap.go
@@ -1,6 +1,6 @@
package localtalk
-import "github.com/pgodw/omnitalk/protocol/llap"
+import "github.com/ObsoleteMadness/ClassicStack/protocol/llap"
// LLAP wire-format types and codes have moved to protocol/llap.
// These aliases keep existing port-internal call sites unchanged while
diff --git a/port/localtalk/localtalk.go b/port/localtalk/localtalk.go
index a9a6341..acc5a3f 100644
--- a/port/localtalk/localtalk.go
+++ b/port/localtalk/localtalk.go
@@ -6,10 +6,10 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
const (
@@ -86,9 +86,9 @@ func (p *Port) ConfigureSendFrame(f func(frame []byte) error) { p.sendFrameFunc
// callers that already pass closures.
func (p *Port) SetFrameSender(fs FrameSender) { p.sendFrameFunc = fs.SendFrame }
-func (p *Port) ShortString() string { return "LocalTalk" }
-func (p *Port) SetLLAPLinkManager(m LinkManager) { p.linkManager = m }
-func (p *Port) SetNodeIDChangeHook(hook func(node uint8)) { p.onNodeIDChange = hook }
+func (p *Port) ShortString() string { return "LocalTalk" }
+func (p *Port) SetLLAPLinkManager(m LinkManager) { p.linkManager = m }
+func (p *Port) SetNodeIDChangeHook(hook func(node uint8)) { p.onNodeIDChange = hook }
func (p *Port) SetCTSResponseTimeout(timeout time.Duration) {
p.mu.Lock()
diff --git a/port/localtalk/ltoudp.go b/port/localtalk/ltoudp.go
index b64bad9..841e9e5 100644
--- a/port/localtalk/ltoudp.go
+++ b/port/localtalk/ltoudp.go
@@ -12,8 +12,8 @@ import (
"golang.org/x/net/ipv4"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
const (
diff --git a/port/localtalk/tashtalk.go b/port/localtalk/tashtalk.go
index 7ff1216..05c4680 100644
--- a/port/localtalk/tashtalk.go
+++ b/port/localtalk/tashtalk.go
@@ -9,10 +9,10 @@ import (
"sync"
"time"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
serial "github.com/jacobsa/go-serial/serial"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
type TashTalkPort struct {
diff --git a/port/nat/ipnat.go b/port/nat/ipnat.go
index 387f51f..626b5b5 100644
--- a/port/nat/ipnat.go
+++ b/port/nat/ipnat.go
@@ -13,10 +13,10 @@ import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
const (
diff --git a/port/port.go b/port/port.go
index 0f90e9d..6956023 100644
--- a/port/port.go
+++ b/port/port.go
@@ -1,6 +1,6 @@
package port
-import "github.com/pgodw/omnitalk/protocol/ddp"
+import "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
type RouterHooks interface {
Inbound(datagram ddp.Datagram, rx Port)
diff --git a/protocol/asp/asp.go b/protocol/asp/asp.go
index 91fbac8..4bd3791 100644
--- a/protocol/asp/asp.go
+++ b/protocol/asp/asp.go
@@ -19,7 +19,7 @@ package asp
import (
"time"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// ---------------------------------------------------------------------------
diff --git a/protocol/atp/atp.go b/protocol/atp/atp.go
index b299d87..3607e37 100644
--- a/protocol/atp/atp.go
+++ b/protocol/atp/atp.go
@@ -14,8 +14,8 @@ import (
"fmt"
"time"
- "github.com/pgodw/omnitalk/pkg/binutil"
- "github.com/pgodw/omnitalk/protocol"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/protocol"
)
// ATP Control bit masks.
diff --git a/protocol/nbp/nbp.go b/protocol/nbp/nbp.go
index 49f2178..d1ae3e5 100644
--- a/protocol/nbp/nbp.go
+++ b/protocol/nbp/nbp.go
@@ -38,7 +38,7 @@ var ErrMalformed = errors.New("nbp: malformed packet")
// Tuple is a single NBP tuple: an address (network/node/socket), an
// enumerator, and an entity name (object:type@zone). Inbound packets
-// carry exactly one tuple in OmniTalk's NBP handler; LkUp-Rply may
+// carry exactly one tuple in ClassicStack's NBP handler; LkUp-Rply may
// pack several but the registered service emits one per match.
type Tuple struct {
Network uint16
diff --git a/protocol/protocol.go b/protocol/protocol.go
index 8acaf73..5498c8a 100644
--- a/protocol/protocol.go
+++ b/protocol/protocol.go
@@ -1,4 +1,4 @@
-// Package protocol defines cross-protocol contracts used by OmniTalk's wire
+// Package protocol defines cross-protocol contracts used by ClassicStack's wire
// implementations (DDP, ATP, ASP, ZIP, RTMP, AEP, LLAP, NBP). Each protocol
// lives in its own subpackage; this package carries only interfaces common to
// all of them.
diff --git a/router/doc.go b/router/doc.go
index b1e15f0..b4ea220 100644
--- a/router/doc.go
+++ b/router/doc.go
@@ -1,4 +1,4 @@
-// Package router implements the OmniTalk AppleTalk Phase 2 router core.
+// Package router implements the ClassicStack AppleTalk Phase 2 router core.
//
// The router maintains the routing table (RTMP) and zone information
// table (ZIP), receives DDP datagrams from every registered Port, and
diff --git a/router/router.go b/router/router.go
index bf6da8e..28c3999 100644
--- a/router/router.go
+++ b/router/router.go
@@ -4,20 +4,20 @@ import (
"context"
"errors"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/telemetry"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/port/localtalk"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/aep"
- "github.com/pgodw/omnitalk/service/llap"
- "github.com/pgodw/omnitalk/service/rtmp"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/telemetry"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/aep"
+ "github.com/ObsoleteMadness/ClassicStack/service/llap"
+ "github.com/ObsoleteMadness/ClassicStack/service/rtmp"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
-var framesInTotal = telemetry.NewCounter("omnitalk_router_frames_in_total")
+var framesInTotal = telemetry.NewCounter("classicstack_router_frames_in_total")
type Router struct {
shortStr string
diff --git a/router/routing_table.go b/router/routing_table.go
index 1e30e1e..5126b1b 100644
--- a/router/routing_table.go
+++ b/router/routing_table.go
@@ -4,8 +4,8 @@ import (
"fmt"
"sync"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
type RoutingTableEntry struct {
diff --git a/router/zone_information_table.go b/router/zone_information_table.go
index 5289105..12d30e5 100644
--- a/router/zone_information_table.go
+++ b/router/zone_information_table.go
@@ -5,7 +5,7 @@ import (
"fmt"
"sync"
- "github.com/pgodw/omnitalk/pkg/encoding"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/encoding"
)
func UCase(input []byte) []byte {
diff --git a/scripts/ci/build.ps1 b/scripts/ci/build.ps1
index fc58ce6..d159716 100644
--- a/scripts/ci/build.ps1
+++ b/scripts/ci/build.ps1
@@ -14,9 +14,9 @@ switch ($buildVariant) {
if ($env:OUTPUT) {
$output = $env:OUTPUT
} elseif ($buildVariant -eq 'all') {
- $output = 'out/omnitalk.exe'
+ $output = 'out/classicstack.exe'
} else {
- $output = "out/omnitalk-$buildVariant.exe"
+ $output = "out/classicstack-$buildVariant.exe"
}
$versionForRc = '0.0.0.0'
@@ -35,14 +35,14 @@ $descriptionSuffix = if ($buildVariant -eq 'all') { '' } else { " ($buildVariant
@"
{
"StringFileInfo": {
- "Comments": "OmniTalk",
+ "Comments": "ClassicStack",
"CompanyName": "ObsoleteMadness",
- "FileDescription": "OmniTalk AppleTalk Router$descriptionSuffix",
+ "FileDescription": "ClassicStack AppleTalk Router$descriptionSuffix",
"FileVersion": "$buildVersion",
- "InternalName": "omnitalk",
+ "InternalName": "classicstack",
"LegalCopyright": "GPL-3.0",
"OriginalFilename": "$exeName",
- "ProductName": "OmniTalk",
+ "ProductName": "ClassicStack",
"ProductVersion": "$buildVersion"
},
"FixedFileInfo": {
@@ -64,14 +64,14 @@ $descriptionSuffix = if ($buildVariant -eq 'all') { '' } else { " ($buildVariant
"FileType": "01",
"FileSubType": "00"
},
- "IconPath": "../../icons/omnitalk.ico"
+ "IconPath": "../../icons/classicstack.ico"
}
-"@ | Set-Content -Path cmd/omnitalk/versioninfo.json -NoNewline
+"@ | Set-Content -Path cmd/classicstack/versioninfo.json -NoNewline
if (-not (Get-Command goversioninfo -ErrorAction SilentlyContinue)) {
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
}
-Push-Location cmd/omnitalk
+Push-Location cmd/classicstack
goversioninfo -64
Pop-Location
@@ -83,7 +83,7 @@ if ($parent) {
$ldflags = "-s -w -X main.BuildVersion=$buildVersion -X main.BuildCommit=$buildCommit -X main.BuildDate=$buildDate"
if ($tags) {
- go build -trimpath -tags $tags -ldflags $ldflags -o $output ./cmd/omnitalk
+ go build -trimpath -tags $tags -ldflags $ldflags -o $output ./cmd/classicstack
} else {
- go build -trimpath -ldflags $ldflags -o $output ./cmd/omnitalk
+ go build -trimpath -ldflags $ldflags -o $output ./cmd/classicstack
}
diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh
index 5b60ad3..9db9af1 100644
--- a/scripts/ci/build.sh
+++ b/scripts/ci/build.sh
@@ -18,9 +18,9 @@ esac
if [[ -n "${OUTPUT:-}" ]]; then
output="$OUTPUT"
elif [[ "$build_variant" == "all" ]]; then
- output="out/omnitalk"
+ output="out/classicstack"
else
- output="out/omnitalk-${build_variant}"
+ output="out/classicstack-${build_variant}"
fi
mkdir -p "$(dirname "$output")"
@@ -28,7 +28,7 @@ mkdir -p "$(dirname "$output")"
ldflags="-s -w -X main.BuildVersion=${build_version} -X main.BuildCommit=${build_commit} -X main.BuildDate=${build_date}"
if [[ -n "$tags" ]]; then
- go build -trimpath -tags "$tags" -ldflags "$ldflags" -o "$output" ./cmd/omnitalk
+ go build -trimpath -tags "$tags" -ldflags "$ldflags" -o "$output" ./cmd/classicstack
else
- go build -trimpath -ldflags "$ldflags" -o "$output" ./cmd/omnitalk
+ go build -trimpath -ldflags "$ldflags" -o "$output" ./cmd/classicstack
fi
diff --git a/scripts/ci/package-release.ps1 b/scripts/ci/package-release.ps1
index b15190b..b56435b 100644
--- a/scripts/ci/package-release.ps1
+++ b/scripts/ci/package-release.ps1
@@ -5,14 +5,14 @@ $buildVariant = if ($env:BUILD_VARIANT) { $env:BUILD_VARIANT } else { 'all' }
if ($buildVariant -eq 'all') {
$variantSlug = ''
- $exeName = 'omnitalk.exe'
+ $exeName = 'classicstack.exe'
} else {
$variantSlug = "-$buildVariant"
- $exeName = "omnitalk-$buildVariant.exe"
+ $exeName = "classicstack-$buildVariant.exe"
}
-$stage = "release/omnitalk$variantSlug-$releaseTag-windows-amd64"
-$archiveName = "omnitalk$variantSlug-$releaseTag-windows-amd64.zip"
+$stage = "release/classicstack$variantSlug-$releaseTag-windows-amd64"
+$archiveName = "classicstack$variantSlug-$releaseTag-windows-amd64.zip"
New-Item -ItemType Directory -Path $stage -Force | Out-Null
Copy-Item "out/$exeName" "$stage/$exeName"
diff --git a/scripts/ci/package-release.sh b/scripts/ci/package-release.sh
index b7be877..d0f4182 100644
--- a/scripts/ci/package-release.sh
+++ b/scripts/ci/package-release.sh
@@ -13,15 +13,15 @@ fi
if [[ "$build_variant" == "all" ]]; then
variant_slug=""
- exe_name="omnitalk"
+ exe_name="classicstack"
else
variant_slug="-${build_variant}"
- exe_name="omnitalk-${build_variant}"
+ exe_name="classicstack-${build_variant}"
fi
if [[ "$target_os" == "linux" ]]; then
- stage="release/omnitalk${variant_slug}-${release_tag}-linux-amd64"
- archive_name="omnitalk${variant_slug}-${release_tag}-linux-amd64.tar.gz"
+ stage="release/classicstack${variant_slug}-${release_tag}-linux-amd64"
+ archive_name="classicstack${variant_slug}-${release_tag}-linux-amd64.tar.gz"
mkdir -p "$stage"
cp "out/${exe_name}" "$stage/${exe_name}"
@@ -33,26 +33,26 @@ if [[ "$target_os" == "linux" ]]; then
fi
if [[ "$target_os" == "macos" ]]; then
- stage="release/omnitalk${variant_slug}-${release_tag}-macos-amd64"
- archive_name="omnitalk${variant_slug}-${release_tag}-macos-amd64.zip"
+ stage="release/classicstack${variant_slug}-${release_tag}-macos-amd64"
+ archive_name="classicstack${variant_slug}-${release_tag}-macos-amd64.zip"
if [[ "$build_variant" == "all" ]]; then
- bundle_name="OmniTalk.app"
+ bundle_name="ClassicStack.app"
else
- bundle_name="OmniTalk-${build_variant}.app"
+ bundle_name="ClassicStack-${build_variant}.app"
fi
app_root="$stage/${bundle_name}/Contents"
mkdir -p "$app_root/MacOS" "$app_root/Resources"
- cp "out/${exe_name}" "$app_root/MacOS/omnitalk"
- chmod +x "$app_root/MacOS/omnitalk"
- cp icons/omnitalk.icns "$app_root/Resources/omnitalk.icns"
+ cp "out/${exe_name}" "$app_root/MacOS/classicstack"
+ chmod +x "$app_root/MacOS/classicstack"
+ cp icons/classicstack.icns "$app_root/Resources/classicstack.icns"
if [[ "$build_variant" == "all" ]]; then
- display_name="OmniTalk"
- bundle_id="com.obsoletemadness.omnitalk"
+ display_name="ClassicStack"
+ bundle_id="com.obsoletemadness.classicstack"
else
- display_name="OmniTalk (${build_variant})"
- bundle_id="com.obsoletemadness.omnitalk.${build_variant}"
+ display_name="ClassicStack (${build_variant})"
+ bundle_id="com.obsoletemadness.classicstack.${build_variant}"
fi
cat > "$app_root/Info.plist" <
CFBundleDisplayName${display_name}
- CFBundleExecutableomnitalk
- CFBundleIconFileomnitalk.icns
+ CFBundleExecutableclassicstack
+ CFBundleIconFileclassicstack.icns
CFBundleIdentifier${bundle_id}
CFBundleName${display_name}
CFBundlePackageTypeAPPL
diff --git a/server.ini b/server.ini
new file mode 100644
index 0000000..f9f18ca
--- /dev/null
+++ b/server.ini
@@ -0,0 +1,84 @@
+[LToUdp]
+; LocalTalk over UDP Settings (used by Mini vMac UDP builds and SNOW emu)
+enabled = true ; Enable LToUDP - true for on, false for off
+seed_network = 1 ; LToUDO seed network number
+seed_zone = "LToUDP Network" ; LToUDP seed zone name.
+
+[TashTalk]
+; TashTalk is a PIC-based RS482 localtalk to serial adaptor
+port = COM6 ; blank to disable, otherwise the serial port to use (eg COM1, /dev/ttyAMA0)
+seed_network = 2 ; TashTalk seed network number
+seed_zone = "TashTalk Network" ; TashTalk seed zone name
+
+[EtherTalk]
+; Ethertalk is a pcap based Network Bridge
+backend = pcap ; supported: pcap, tap, tun. Leave blank to disable ethertalk.
+device = "\Device\NPF_{B7D4E073-2185-4912-BBE8-3948C6636D02}" ; PCap device name. Blank to disable ethertalk. Call with -list-pcap-devices to see what to use. Linux /dev/eth0. Windows: "\Device\NPF_{B7D4E073-2185-4912-BBE8-3948C6636D02}".
+;device = "\Device\NPF_{7A63BBB0-EBC1-4FA7-A397-8E7F42E39A73}" ; PCap device name. Blank to disable ethertalk. Call with -list-pcap-devices to see what to use. Linux /dev/eth0. Windows: "\Device\NPF_{B7D4E073-2185-4912-BBE8-3948C6636D02}".
+hw_address = "DE:AD:BE:EF:CA:FE" ; EtherTalk Hardware Address to use for router.
+seed_network_min = 3 ; EtherTalk seed network number
+seed_network_max = 5 ; EtherTalk seed network
+seed_zone = "EtherTalk Network" ; EtherTalk seed zone name
+bridge_mode = auto ; auto (default), ethernet, or wifi. Use wifi for bridge-shim rewriting on Wi-Fi adapters.
+bridge_host_mac = ; optional host adapter MAC for Wi-Fi bridge shim. Defaults to hw_address when blank.
+
+
+[MacIP]
+; MacIP Gateway Settings. Allows TCP over DDP.
+enabled = true ; true to enable MacIP Gateway, false to disable
+mode = pcap ; modes are pcap or nat.
+zone = ; MacIP Gateway Zone, defaults to EtherTalk zone, otherwise the first zone detected.
+nat_subnet = ; in NAT mode, the subnet to use (eg 192.168.100.0/24)
+nat_gw = ; in NAT mode, the IP Address to use for the gateway (eg 192.168.100.1)
+lease_file = leases.txt ; in NAT mode, persist DHCP leases to the specified file
+ip_gateway = ; Upstream/default gateway on the IP-side network
+dhcp_relay = true ; DHCP Relay, converts MacTCP Auto Config to DHCP requests
+nameserver = 1.1.1.1 ; Name server for DNS
+
+
+[AFP]
+; Apple Filing Protocol Server Settings
+enabled = true ; true to enable AFP Server, false to disable
+name = "ClassicStack" ; Name of the server to use. Max length of 31 characters.
+zone = "EtherTalk Network" ; Name of the AppleTalk Zone to list the server in
+protocols = ddp,tcp ; Protocols to use. Supports ddp (AppleTalk) and tcp (TCP/IP). They can be combined (eg ddp,tcp)
+binding = ":548" ; When TCP is enabled, the IP+Port to bind the service to.
+extension_map = "extmap.conf" ; Netatalk compatible extension mapping file
+
+[Volumes.Default]
+name = "Welcome"
+path = "./dist/Sample Volume"
+read_only = true
+
+[Volumes.TestVolume]
+; AFP Volume Configuration. Each volume must have a section for this.
+name = "Test Volume" ; Volume Name. Max Length of 31 characters.
+path = "C:\Mac\Test" ; Host path for the volume. Eg "/media/Mac", "C:\Foo"
+cnid_backend = ; leave blank for default. Default is "memory" and is currently the only mode supported
+use_decomposed_names = true ; Encode host-reserved filename characters using 0xNN tokens when mapping AFP paths. Default is true.
+fork_backend = AppleDouble ; Fork backend to use. Currently only "AppleDouble" is implemented.
+appledouble_mode = "modern" ; AppleDouble mode to use if using AppleDouble. Supported options are "legacy" and "modern".
+ ; Legacy is the NetaTalk 2.x ".appledouble" folder approach.
+ ; Modern is the NetaTalk 4.x method of "._" side cars. Default is "modern".
+rebuild_desktop_db = false ; When true, rebuilds the desktop database from resource forks. Default is false.
+
+[Volumes.Volume68k]
+; AFP Volume Configuration. Each volume must have a section for this.
+name = "Volume 68K" ; Volume Name. Max Length of 31 characters.
+path = "C:\Mac\Volume68K" ; Host path for the volume. Eg "/media/Mac", "C:\Foo"
+cnid_backend = ; leave blank for default. Default is "memory" and is currently the only mode supported
+use_decomposed_names = true ; Encode host-reserved filename characters using 0xNN tokens when mapping AFP paths. Default is true.
+fork_backend = AppleDouble ; Fork backend to use. Currently only "AppleDouble" is implemented.
+appledouble_mode = "legacy" ; AppleDouble mode to use if using AppleDouble. Supported options are "legacy" and "modern".
+ ; Legacy is the NetaTalk 2.x ".appledouble" folder approach.
+ ; Modern is the NetaTalk 4.x method of "._" side cars. Default is "modern".
+rebuild_desktop_db = false ; When true, rebuilds the desktop database from resource forks.
+
+
+[Logging]
+level = debug
+parse_packets = true
+log_traffic = true
+
+
+
diff --git a/server.toml b/server.toml
index 6fdc912..8f0dcf1 100644
--- a/server.toml
+++ b/server.toml
@@ -37,7 +37,7 @@ nameserver = "1.1.1.1" # DNS nameserver
[AFP]
# Apple Filing Protocol server settings
enabled = true
-name = "OmniTalk" # Server name. Max 31 characters.
+name = "ClassicStack" # Server name. Max 31 characters.
zone = "EtherTalk Network"
protocols = "ddp,tcp" # Comma-separated: ddp, tcp, or both
binding = ":548"
diff --git a/server.toml.example b/server.toml.example
index 8be1d6b..3544700 100644
--- a/server.toml.example
+++ b/server.toml.example
@@ -40,7 +40,7 @@ nameserver = "1.1.1.1" # DNS nameserver
[AFP]
# Apple Filing Protocol server settings
enabled = true # true to enable AFP server
-name = "OmniTalk" # Server name. Max 31 characters.
+name = "ClassicStack" # Server name. Max 31 characters.
zone = "EtherTalk Network" # AppleTalk zone to advertise the server in
protocols = "ddp,tcp" # Comma-separated: ddp, tcp, or both
binding = ":548" # When TCP is enabled, the bind address
diff --git a/service/aep/aep.go b/service/aep/aep.go
index 4196772..231ed13 100644
--- a/service/aep/aep.go
+++ b/service/aep/aep.go
@@ -1,5 +1,5 @@
/*
-Package aep implements the AppleTalk Echo Protocol (AEP) as a omnitalk service.
+Package aep implements the AppleTalk Echo Protocol (AEP) as a classicstack service.
AEP uses DDP type 4 on socket 4. An echo request (command byte 1) is reflected
back to the sender as an echo reply (command byte 2).
@@ -12,11 +12,11 @@ import (
"context"
"sync"
- "github.com/pgodw/omnitalk/protocol/aep"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/aep"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// Socket is the well-known AEP socket number, re-exported from protocol/aep
diff --git a/service/afp/appledouble_backend.go b/service/afp/appledouble_backend.go
index 08a477e..f9535b9 100644
--- a/service/afp/appledouble_backend.go
+++ b/service/afp/appledouble_backend.go
@@ -11,7 +11,7 @@ import (
"path/filepath"
"strings"
- "github.com/pgodw/omnitalk/pkg/appledouble"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/appledouble"
)
const defaultAppleDoubleMode = AppleDoubleModeModern
diff --git a/service/afp/appledouble_lifecycle_test.go b/service/afp/appledouble_lifecycle_test.go
index 05d1fc0..bb54935 100644
--- a/service/afp/appledouble_lifecycle_test.go
+++ b/service/afp/appledouble_lifecycle_test.go
@@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"
- "github.com/pgodw/omnitalk/pkg/appledouble"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/appledouble"
)
func TestHandleRename_MovesAppleDoubleSidecar(t *testing.T) {
diff --git a/service/afp/catsearch.go b/service/afp/catsearch.go
index aea308e..33be325 100644
--- a/service/afp/catsearch.go
+++ b/service/afp/catsearch.go
@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
- "github.com/pgodw/omnitalk/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
// catSearchMaxDataLen is the maximum bytes of ResultsRecord data per reply.
diff --git a/service/afp/cnid.go b/service/afp/cnid.go
index 23e5cd7..b2ea57f 100644
--- a/service/afp/cnid.go
+++ b/service/afp/cnid.go
@@ -3,8 +3,8 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/cnid"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/cnid"
)
// CNID constants and the Store interface now live in pkg/cnid. These
diff --git a/service/afp/desktop.go b/service/afp/desktop.go
index 13d0179..0535729 100644
--- a/service/afp/desktop.go
+++ b/service/afp/desktop.go
@@ -8,7 +8,7 @@ import (
"io/fs"
"path/filepath"
- "github.com/pgodw/omnitalk/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
// getDesktopDB looks up the DesktopDB associated with a DTRefNum. The
diff --git a/service/afp/desktop_models.go b/service/afp/desktop_models.go
index 160a0d5..72bbe29 100644
--- a/service/afp/desktop_models.go
+++ b/service/afp/desktop_models.go
@@ -6,7 +6,7 @@ import (
"encoding/binary"
"fmt"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// FPOpenDT - open the Desktop Database for a volume.
diff --git a/service/afp/desktop_rebuild.go b/service/afp/desktop_rebuild.go
index 870ebf8..fe3c6c7 100644
--- a/service/afp/desktop_rebuild.go
+++ b/service/afp/desktop_rebuild.go
@@ -14,8 +14,8 @@ import (
"path/filepath"
"strings"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/appledouble"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/appledouble"
)
// EnableAppleDoubleIconFallback controls whether FPGetIcon misses trigger a
diff --git a/service/afp/desktopdb.go b/service/afp/desktopdb.go
index edc5e19..f143105 100644
--- a/service/afp/desktopdb.go
+++ b/service/afp/desktopdb.go
@@ -7,8 +7,8 @@ import (
"fmt"
"sync"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/cnid"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/cnid"
)
const desktopDBFilename = ".desktop.db"
diff --git a/service/afp/directory.go b/service/afp/directory.go
index b06c6b5..baa16c7 100644
--- a/service/afp/directory.go
+++ b/service/afp/directory.go
@@ -3,12 +3,13 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"bytes"
"errors"
"io/fs"
"os"
"path/filepath"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
func (s *Service) handleOpenDir(req *FPOpenDirReq) (*FPOpenDirRes, int32) {
diff --git a/service/afp/directory_models.go b/service/afp/directory_models.go
index 5061545..48f7cdb 100644
--- a/service/afp/directory_models.go
+++ b/service/afp/directory_models.go
@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
func formatDirBitmap(bitmap uint16) string {
diff --git a/service/afp/dispatcher.go b/service/afp/dispatcher.go
index f12b39f..3a933a7 100644
--- a/service/afp/dispatcher.go
+++ b/service/afp/dispatcher.go
@@ -5,7 +5,7 @@ package afp
import (
"runtime/debug"
- "github.com/pgodw/omnitalk/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
// Request is the decoded form of an inbound AFP command.
diff --git a/service/afp/enumerate_encoding_test.go b/service/afp/enumerate_encoding_test.go
index 32fd59e..e5710a8 100644
--- a/service/afp/enumerate_encoding_test.go
+++ b/service/afp/enumerate_encoding_test.go
@@ -12,7 +12,7 @@ import (
"testing"
"time"
- "github.com/pgodw/omnitalk/pkg/encoding"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/encoding"
)
type enumStubInfo struct {
diff --git a/service/afp/file.go b/service/afp/file.go
index 7ba1ec1..5db889b 100644
--- a/service/afp/file.go
+++ b/service/afp/file.go
@@ -3,11 +3,12 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"errors"
"io"
"os"
"path/filepath"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
func (s *Service) handleSetFileParms(req *FPSetFileParmsReq) (*FPSetFileParmsRes, int32) {
diff --git a/service/afp/filedir.go b/service/afp/filedir.go
index 2d5d79d..2c11661 100644
--- a/service/afp/filedir.go
+++ b/service/afp/filedir.go
@@ -3,10 +3,11 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"bytes"
"io/fs"
"path/filepath"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
func (s *Service) handleGetFileDirParms(req *FPGetFileDirParmsReq) (*FPGetFileDirParmsRes, int32) {
diff --git a/service/afp/filedir_models.go b/service/afp/filedir_models.go
index d3fc363..72b4c7b 100644
--- a/service/afp/filedir_models.go
+++ b/service/afp/filedir_models.go
@@ -6,7 +6,7 @@ import (
"encoding/binary"
"fmt"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
type FPGetFileDirParmsReq struct {
diff --git a/service/afp/filedir_pack.go b/service/afp/filedir_pack.go
index 8629769..fa55100 100644
--- a/service/afp/filedir_pack.go
+++ b/service/afp/filedir_pack.go
@@ -8,7 +8,7 @@ import (
"path/filepath"
"time"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// toAFPTime converts a Go time.Time to AFP's seconds-since-1904 epoch.
diff --git a/service/afp/fork.go b/service/afp/fork.go
index c4d2a93..bd9f3fb 100644
--- a/service/afp/fork.go
+++ b/service/afp/fork.go
@@ -13,9 +13,9 @@ import (
"path/filepath"
"syscall"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/appledouble"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/appledouble"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
func (s *Service) handleOpenFork(req *FPOpenForkReq) (*FPOpenForkRes, int32) {
diff --git a/service/afp/fork_models.go b/service/afp/fork_models.go
index c8db4a6..cf59cad 100644
--- a/service/afp/fork_models.go
+++ b/service/afp/fork_models.go
@@ -6,7 +6,7 @@ import (
"encoding/binary"
"fmt"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// Fork type constants for FPOpenFork.
diff --git a/service/afp/info.go b/service/afp/info.go
index 5beea0d..a4f555c 100644
--- a/service/afp/info.go
+++ b/service/afp/info.go
@@ -5,7 +5,7 @@ package afp
import (
"bytes"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// BuildServerInfo constructs the payload for an AFP FPGetSrvrInfo or ASP GetStatus reply.
diff --git a/service/afp/logging.go b/service/afp/logging.go
index 7de473c..d185667 100644
--- a/service/afp/logging.go
+++ b/service/afp/logging.go
@@ -3,8 +3,9 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"fmt"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
func (s *Service) logPacket(format string, args ...any) {
diff --git a/service/afp/macgarden_fs.go b/service/afp/macgarden_fs.go
new file mode 100644
index 0000000..83ef2e0
--- /dev/null
+++ b/service/afp/macgarden_fs.go
@@ -0,0 +1,1810 @@
+//go:build macgarden
+
+package afp
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+ "unicode"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ garden "github.com/ObsoleteMadness/ClassicStack/service/macgarden"
+)
+
+const macGardenEnumerateWindow = 10
+const macGardenSearchPageSize = 20
+
+type macGardenFileInfo struct {
+ name string
+ size int64
+ mode fs.FileMode
+ modTime time.Time
+ isDir bool
+}
+
+func (i *macGardenFileInfo) Name() string { return i.name }
+func (i *macGardenFileInfo) Size() int64 { return i.size }
+func (i *macGardenFileInfo) Mode() fs.FileMode { return i.mode }
+func (i *macGardenFileInfo) ModTime() time.Time { return i.modTime }
+func (i *macGardenFileInfo) IsDir() bool { return i.isDir }
+func (i *macGardenFileInfo) Sys() any { return nil }
+
+type macGardenDirEntry struct{ info fs.FileInfo }
+
+func (d macGardenDirEntry) Name() string { return d.info.Name() }
+func (d macGardenDirEntry) IsDir() bool { return d.info.IsDir() }
+func (d macGardenDirEntry) Type() fs.FileMode { return d.info.Mode().Type() }
+func (d macGardenDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
+
+type macGardenCachedResult struct {
+ Name string
+ URL string
+}
+
+type macGardenAsset struct {
+ Name string
+ URL string
+ Size int64
+ Content []byte
+}
+
+type macGardenCategoryPageMeta struct {
+ TotalCount uint16
+ PageSize int
+ LastPageNumber int
+ LastPageCount int
+}
+
+type macGardenFile struct {
+ asset macGardenAsset
+ client *garden.Client
+}
+
+func (f *macGardenFile) ReadAt(p []byte, off int64) (n int, err error) {
+ if off < 0 {
+ return 0, fs.ErrInvalid
+ }
+ if len(f.asset.Content) > 0 {
+ if off >= int64(len(f.asset.Content)) {
+ return 0, io.EOF
+ }
+ n = copy(p, f.asset.Content[off:])
+ if n < len(p) {
+ return n, io.EOF
+ }
+ return n, nil
+ }
+ // ReadURLRange applies the client's maxRangeSize cap internally, so it may
+ // return fewer bytes than len(p). Signal io.EOF only when the HTTP response
+ // is shorter than the bytes we actually requested — meaning we hit real EOF,
+ // not just the range cap. FPRead buffers are already bounded by the same cap
+ // (via handleRead.maxReadSize), so for that path len(data)==len(p) always.
+ // FPCopyFile re-reads in a loop, so getting n 0 && requested > max {
+ requested = max
+ }
+ data, readErr := f.client.ReadURLRange(f.asset.URL, off, len(p))
+ if readErr != nil {
+ return 0, fmt.Errorf("%w: %v", ErrCopySourceReadEOF, readErr)
+ }
+ n = copy(p, data)
+ if len(data) < requested {
+ return n, io.EOF
+ }
+ return n, nil
+}
+
+func (f *macGardenFile) WriteAt(_ []byte, _ int64) (n int, err error) { return 0, fs.ErrPermission }
+func (f *macGardenFile) Truncate(_ int64) error { return fs.ErrPermission }
+func (f *macGardenFile) Close() error { return nil }
+func (f *macGardenFile) Sync() error { return nil }
+func (f *macGardenFile) Stat() (fs.FileInfo, error) {
+ size := f.asset.Size
+ if size == 0 && f.asset.URL != "" {
+ if s, err := f.client.GetContentLength(f.asset.URL); err == nil {
+ size = s
+ }
+ }
+ return &macGardenFileInfo{name: filepath.Base(f.asset.Name), size: size, mode: 0o444, modTime: time.Now().UTC()}, nil
+}
+
+// fetchAndCacheScreenshot downloads a screenshot URL and stores it in the
+// in-memory cache. Subsequent OpenFile calls serve from cache without network I/O.
+func (m *MacGardenFileSystem) fetchAndCacheScreenshot(url string) ([]byte, error) {
+ m.screenshotMu.RLock()
+ if data, ok := m.screenshotCache[url]; ok {
+ m.screenshotMu.RUnlock()
+ return data, nil
+ }
+ m.screenshotMu.RUnlock()
+ data, err := m.client.FetchFull(url)
+ if err != nil {
+ return nil, err
+ }
+ m.screenshotMu.Lock()
+ m.screenshotCache[url] = data
+ m.screenshotMu.Unlock()
+ return data, nil
+}
+
+// resolveAssetSize returns the known size, or triggers a size fetch appropriate
+// for the asset type. Called during FPGetFileDirParms so Finder sees the real size.
+// Screenshots: full download cached in memory (avoids HEAD which gets blocked).
+// Downloads: ranged GET to read the Content-Range total only.
+func (m *MacGardenFileSystem) resolveAssetSize(a macGardenAsset) int64 {
+ if a.Size > 0 || a.URL == "" {
+ return a.Size
+ }
+ if strings.HasPrefix(a.Name, "Screenshots/") {
+ if data, err := m.fetchAndCacheScreenshot(a.URL); err == nil {
+ return int64(len(data))
+ }
+ return 0
+ }
+ if s, err := m.client.GetContentLength(a.URL); err == nil {
+ return s
+ }
+ return 0
+}
+
+// MacGardenFileSystem is a read-only virtual filesystem backed by macintoshgarden.org.
+type macGardenSearchCache struct {
+ pages map[int][]garden.SearchResult // pageNumber -> results
+ exhausted bool // true when all pages have been fetched
+}
+
+type MacGardenFileSystem struct {
+ root string
+ client *garden.Client
+
+ mu sync.RWMutex
+ categories []garden.Category
+ searchByName map[string]macGardenCachedResult
+ itemURLByDir map[string]string
+ itemByURL map[string]*garden.SoftwareItem
+ itemsInCategory map[string][]garden.SearchResult // categoryURL -> items
+ categoryItemCount map[string]uint16
+ categoryPageMeta map[string]macGardenCategoryPageMeta
+ categoryPageItems map[string]map[int][]garden.SearchResult
+ downloadByPath map[string]macGardenAsset
+ screenshotByPath map[string]macGardenAsset
+ descriptionByPath map[string]macGardenAsset
+ catSearchCache map[string]*macGardenSearchCache // normalized query -> cached results
+
+ screenshotMu sync.RWMutex
+ screenshotCache map[string][]byte // URL -> full image bytes
+
+ stop chan struct{}
+ stopOnce sync.Once
+ wg sync.WaitGroup
+}
+
+func init() {
+ RegisterFS(FSTypeMacGarden, func(cfg VolumeConfig) (FileSystem, error) {
+ return NewMacGardenFileSystem(filepath.Clean(cfg.Path)), nil
+ })
+}
+
+func NewMacGardenFileSystem(root string) *MacGardenFileSystem {
+ fsys := &MacGardenFileSystem{
+ root: filepath.Clean(root),
+ client: garden.NewClient(),
+ searchByName: make(map[string]macGardenCachedResult),
+ itemURLByDir: make(map[string]string),
+ itemByURL: make(map[string]*garden.SoftwareItem),
+ itemsInCategory: make(map[string][]garden.SearchResult),
+ categoryItemCount: make(map[string]uint16),
+ categoryPageMeta: make(map[string]macGardenCategoryPageMeta),
+ categoryPageItems: make(map[string]map[int][]garden.SearchResult),
+ downloadByPath: make(map[string]macGardenAsset),
+ screenshotByPath: make(map[string]macGardenAsset),
+ descriptionByPath: make(map[string]macGardenAsset),
+ catSearchCache: make(map[string]*macGardenSearchCache),
+ screenshotCache: make(map[string][]byte),
+ stop: make(chan struct{}),
+ }
+ fsys.loadCategories()
+ return fsys
+}
+
+func (m *MacGardenFileSystem) loadCategories() {
+ m.mu.RLock()
+ if len(m.categories) > 0 {
+ m.mu.RUnlock()
+ return
+ }
+ m.mu.RUnlock()
+ cats, err := m.client.GetCategories()
+ if err != nil {
+ netlog.Warn("[AFP][MacGarden] failed to fetch categories: %v", err)
+ return
+ }
+ sort.Slice(cats, func(i, j int) bool { return strings.ToLower(cats[i].Name) < strings.ToLower(cats[j].Name) })
+ m.mu.Lock()
+ if len(m.categories) == 0 {
+ m.categories = cats
+ }
+ m.mu.Unlock()
+ if len(cats) == 0 {
+ netlog.Warn("[AFP][MacGarden] category fetch succeeded but returned no categories")
+ }
+}
+
+func (m *MacGardenFileSystem) normalize(path string) (string, error) {
+ clean := filepath.Clean(path)
+ rel, err := filepath.Rel(m.root, clean)
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
+ return "", fs.ErrPermission
+ }
+ if rel == "." {
+ return "", nil
+ }
+ return filepath.ToSlash(rel), nil
+}
+
+// readDirCore resolves a normalized relative path to directory entries. It is
+// the shared implementation used by both ReadDir and ReadDirRange. Callers are
+// responsible for running it in a goroutine if a timeout is needed.
+func (m *MacGardenFileSystem) readDirCore(rel string) ([]fs.DirEntry, error) {
+ if rel == "" {
+ netlog.Debug("[AFP][MacGarden] ReadDir root")
+ return []fs.DirEntry{
+ macGardenDirEntry{info: &macGardenFileInfo{name: "Apps", mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}},
+ macGardenDirEntry{info: &macGardenFileInfo{name: "Games", mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}},
+ macGardenDirEntry{info: &macGardenFileInfo{name: "search", mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}},
+ }, nil
+ }
+
+ parts := strings.Split(rel, "/")
+
+ // Apps or Games level: show categories for that type.
+ if len(parts) == 1 && (parts[0] == "Apps" || parts[0] == "Games") {
+ netlog.Debug("[AFP][MacGarden] ReadDir %s", parts[0])
+ m.loadCategories()
+ catType := parts[0]
+ urlPrefix := "/apps/"
+ if catType == "Games" {
+ urlPrefix = "/games/"
+ }
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ entries := make([]fs.DirEntry, 0, len(m.categories))
+ for _, cat := range m.categories {
+ if strings.HasPrefix(strings.ToLower(urlPathFromAbsolute(cat.URL)), urlPrefix) {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: cat.Name, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ }
+ netlog.Info("[AFP][MacGarden] ReadDir %s returning %d entries", catType, len(entries))
+ return entries, nil
+ }
+
+ // /search — list all cached search queries as subdirectories.
+ if len(parts) == 1 && parts[0] == "search" {
+ m.mu.RLock()
+ queries := make([]string, 0, len(m.catSearchCache))
+ for q := range m.catSearchCache {
+ queries = append(queries, q)
+ }
+ m.mu.RUnlock()
+ sort.Strings(queries)
+ entries := make([]fs.DirEntry, 0, len(queries))
+ for _, q := range queries {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: q, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ return entries, nil
+ }
+
+ // /search/ — list type subdirectories (App, Game) plus untyped items.
+ if len(parts) == 2 && parts[0] == "search" {
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ pageNums := make([]int, 0, len(cache.pages))
+ for k := range cache.pages {
+ pageNums = append(pageNums, k)
+ }
+ sort.Ints(pageNums)
+ typesSeen := map[string]struct{}{}
+ untypedSeen := map[string]struct{}{}
+ var typeNames, untypedNames []string
+ for _, pn := range pageNums {
+ for _, r := range cache.pages[pn] {
+ if r.Type != "" {
+ if _, exists := typesSeen[r.Type]; !exists {
+ typesSeen[r.Type] = struct{}{}
+ typeNames = append(typeNames, r.Type)
+ }
+ } else {
+ if name := sanitizeGardenName(r.Name); name != "" {
+ if _, exists := untypedSeen[name]; !exists {
+ untypedSeen[name] = struct{}{}
+ untypedNames = append(untypedNames, name)
+ }
+ }
+ }
+ }
+ }
+ sort.Strings(typeNames)
+ sort.Strings(untypedNames)
+ entries := make([]fs.DirEntry, 0, len(typeNames)+len(untypedNames))
+ for _, name := range typeNames {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: name, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ for _, name := range untypedNames {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: name, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ return entries, nil
+ }
+
+ // /search// — virtual type subdirectory (App/Game).
+ if len(parts) == 3 && parts[0] == "search" && isSearchResultType(parts[2]) {
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ resultType := parts[2]
+ var names []string
+ for _, page := range cache.pages {
+ for _, r := range page {
+ if r.Type == resultType {
+ if name := sanitizeGardenName(r.Name); name != "" {
+ names = append(names, name)
+ }
+ }
+ }
+ }
+ sort.Strings(names)
+ entries := make([]fs.DirEntry, 0, len(names))
+ for _, name := range names {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: name, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ return entries, nil
+ }
+
+ // /search//- — assets for that item.
+ if len(parts) == 3 && parts[0] == "search" {
+ itemName := parts[2]
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, search.URL); err != nil {
+ return nil, err
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ return buildItemDirEntries(assets, ""), nil
+ }
+
+ // /search///
- [/] — typed item or its subdirectory.
+ if len(parts) >= 4 && parts[0] == "search" && isSearchResultType(parts[2]) {
+ itemName := parts[3]
+ subPath := filepath.ToSlash(filepath.Join(parts[4:]...))
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, search.URL); err != nil {
+ return nil, err
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ return buildItemDirEntries(assets, subPath), nil
+ }
+
+ // /search//
- / — subdirectory within an item.
+ if len(parts) >= 4 && parts[0] == "search" {
+ itemName := parts[2]
+ subPath := filepath.ToSlash(filepath.Join(parts[3:]...))
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, search.URL); err != nil {
+ return nil, err
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ return buildItemDirEntries(assets, subPath), nil
+ }
+
+ // Apps/Games/CategoryName/ItemName — assets for a software item
+ if len(parts) == 3 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catName, itemName := parts[1], parts[2]
+ catURL := m.getCategoryURL(catName)
+ if catURL == "" {
+ return nil, fs.ErrNotExist
+ }
+ itemURL, err := m.getItemURLInCategory(catURL, itemName)
+ if err != nil {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, itemURL); err != nil {
+ return nil, err
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ return buildItemDirEntries(assets, ""), nil
+ }
+
+ // Apps/Games/CategoryName/ItemName/SubDir... — subdirectory within an item
+ if len(parts) >= 4 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catName, itemName := parts[1], parts[2]
+ subPath := filepath.ToSlash(filepath.Join(parts[3:]...))
+ catURL := m.getCategoryURL(catName)
+ if catURL == "" {
+ return nil, fs.ErrNotExist
+ }
+ itemURL, err := m.getItemURLInCategory(catURL, itemName)
+ if err != nil {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, itemURL); err != nil {
+ return nil, err
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ return buildItemDirEntries(assets, subPath), nil
+ }
+
+ return nil, fs.ErrNotExist
+}
+
+func (m *MacGardenFileSystem) ReadDir(path string) ([]fs.DirEntry, error) {
+ rel, err := m.normalize(path)
+ if err != nil {
+ return nil, err
+ }
+ return m.readDirCore(rel)
+}
+
+func (m *MacGardenFileSystem) ReadDirRange(path string, startIndex uint16, reqCount uint16) ([]fs.DirEntry, uint16, error) {
+ if reqCount == 0 {
+ return nil, 0, nil
+ }
+ rel, err := m.normalize(path)
+ if err != nil {
+ return nil, 0, err
+ }
+ parts := strings.Split(rel, "/")
+ if len(parts) == 1 && (parts[0] == "Apps" || parts[0] == "Games") {
+ m.loadCategories()
+ prefix := "/apps/"
+ if parts[0] == "Games" {
+ prefix = "/games/"
+ }
+ m.mu.RLock()
+ filtered := make([]fs.DirEntry, 0, len(m.categories))
+ for _, cat := range m.categories {
+ if strings.HasPrefix(strings.ToLower(urlPathFromAbsolute(cat.URL)), prefix) {
+ filtered = append(filtered, macGardenDirEntry{info: &macGardenFileInfo{name: cat.Name, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ }
+ m.mu.RUnlock()
+ total := uint16(len(filtered))
+ if startIndex < 1 {
+ startIndex = 1
+ }
+ if int(startIndex) > len(filtered) {
+ return nil, total, nil
+ }
+ start := int(startIndex) - 1
+ end := start + int(reqCount)
+ if end > len(filtered) {
+ end = len(filtered)
+ }
+ return append([]fs.DirEntry(nil), filtered[start:end]...), total, nil
+ }
+ if len(parts) == 2 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catURL := m.getCategoryURL(parts[1])
+ if catURL == "" {
+ return nil, 0, fs.ErrNotExist
+ }
+ return m.readCategoryDirRange(catURL, startIndex, reqCount)
+ }
+ entries, err := m.readDirCore(rel)
+ if err != nil {
+ return nil, 0, err
+ }
+ total := uint16(len(entries))
+ if startIndex < 1 {
+ startIndex = 1
+ }
+ if int(startIndex) > len(entries) {
+ return nil, total, nil
+ }
+ start := int(startIndex) - 1
+ end := start + int(reqCount)
+ if end > len(entries) {
+ end = len(entries)
+ }
+ return append([]fs.DirEntry(nil), entries[start:end]...), total, nil
+}
+
+func (m *MacGardenFileSystem) Stat(path string) (fs.FileInfo, error) {
+ rel, err := m.normalize(path)
+ if err != nil {
+ return nil, err
+ }
+ if rel == "" {
+ return &macGardenFileInfo{name: filepath.Base(m.root), mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+
+ parts := strings.Split(rel, "/")
+
+ // Apps or Games level
+ if len(parts) == 1 && (parts[0] == "Apps" || parts[0] == "Games") {
+ return &macGardenFileInfo{name: parts[0], mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+
+ // /search virtual directory
+ if len(parts) == 1 && parts[0] == "search" {
+ return &macGardenFileInfo{name: "search", mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+
+ // /search/
+ if len(parts) == 2 && parts[0] == "search" {
+ m.mu.RLock()
+ _, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if ok {
+ return &macGardenFileInfo{name: parts[1], mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ return nil, fs.ErrNotExist
+ }
+
+ // /search// — virtual type subdirectory (App/Game)
+ // /search//
- — item directory
+ if len(parts) == 3 && parts[0] == "search" {
+ if isSearchResultType(parts[2]) {
+ return &macGardenFileInfo{name: parts[2], mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ itemName := parts[2]
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ for _, page := range cache.pages {
+ for _, r := range page {
+ if sanitizeGardenName(r.Name) == itemName {
+ return &macGardenFileInfo{name: itemName, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ }
+ }
+ return nil, fs.ErrNotExist
+ }
+
+ // /search///
- [/] or /search//
- /
+ if len(parts) >= 4 && parts[0] == "search" {
+ var itemName, fileName string
+ if isSearchResultType(parts[2]) {
+ itemName = parts[3]
+ fileName = strings.Join(parts[4:], "/")
+ } else {
+ itemName = parts[2]
+ fileName = strings.Join(parts[3:], "/")
+ }
+ if fileName == "" {
+ // It's the item directory itself under a type subdirectory
+ return &macGardenFileInfo{name: itemName, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ loaded := false
+ if ok {
+ _, loaded = m.itemByURL[search.URL]
+ }
+ m.mu.RUnlock()
+ if !ok || !loaded {
+ return nil, fs.ErrNotExist
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ for _, a := range assets {
+ if a.Name == fileName {
+ return &macGardenFileInfo{name: filepath.Base(a.Name), size: m.resolveAssetSize(a), mode: 0o444, modTime: time.Now().UTC()}, nil
+ }
+ }
+ prefix := fileName + "/"
+ for _, a := range assets {
+ if strings.HasPrefix(a.Name, prefix) {
+ return &macGardenFileInfo{name: filepath.Base(fileName), mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ }
+ return nil, fs.ErrNotExist
+ }
+
+ // Search-hit item directory at root level (legacy, retained for compatibility).
+ if len(parts) == 1 {
+ m.mu.RLock()
+ _, ok := m.searchByName[parts[0]]
+ m.mu.RUnlock()
+ if ok {
+ return &macGardenFileInfo{name: parts[0], mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ }
+
+ // Category level - return immediately without fetching items
+ // Stat should be lightweight; items are fetched lazily only on ReadDir
+ if len(parts) == 2 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catName := parts[1]
+ catURL := m.getCategoryURL(catName)
+ if catURL != "" {
+ netlog.Debug("[AFP][MacGarden] Stat returning category (no lazy fetch): %s", catName)
+ return &macGardenFileInfo{name: catName, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ return nil, fs.ErrNotExist
+ }
+
+ // Item level - return immediately without fetching items
+ if len(parts) == 3 && (parts[0] == "Apps" || parts[0] == "Games") {
+ itemName := parts[2]
+ // Don't fetch the item here; just return dir info
+ // Real items are fetched lazily when ReadDir is called
+ netlog.Debug("[AFP][MacGarden] Stat returning item (no lazy fetch): %s", itemName)
+ return &macGardenFileInfo{name: itemName, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+
+ // macOS probes certain well-known system paths on every directory it visits.
+ // Reject them quickly so we never trigger network fetches for them.
+ macSystemNames := map[string]bool{
+ "Configuration": true,
+ "Network Trash Folder": true,
+ "TheVolumeSettingsFolder": true,
+ "Temporary Items": true,
+ ".DS_Store": true,
+ "Icon\r": true,
+ }
+ if len(parts) >= 3 && macSystemNames[parts[len(parts)-1]] {
+ return nil, fs.ErrNotExist
+ }
+
+ // Asset level (file)
+ if len(parts) >= 4 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catName := parts[1]
+ itemName := parts[2]
+ fileName := strings.Join(parts[3:], "/")
+
+ catURL := m.getCategoryURL(catName)
+ if catURL == "" {
+ return nil, fs.ErrNotExist
+ }
+
+ itemURL, err := m.getItemURLInCategory(catURL, itemName)
+ if err != nil {
+ return nil, fs.ErrNotExist
+ }
+
+ // Keep Stat lazy for item children: if the item has not been opened yet,
+ // do not fetch details just to probe a potential child path.
+ m.mu.RLock()
+ _, loaded := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if !loaded {
+ return nil, fs.ErrNotExist
+ }
+
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, a := range assets {
+ if a.Name == fileName {
+ return &macGardenFileInfo{name: filepath.Base(a.Name), size: m.resolveAssetSize(a), mode: 0o444, modTime: time.Now().UTC()}, nil
+ }
+ }
+ prefix := fileName + "/"
+ for _, a := range assets {
+ if strings.HasPrefix(a.Name, prefix) {
+ return &macGardenFileInfo{name: filepath.Base(fileName), mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}, nil
+ }
+ }
+ }
+
+ // Asset-level file under root search-hit item dir: ItemName/Asset
+ if len(parts) >= 2 && parts[0] != "Apps" && parts[0] != "Games" {
+ itemName := parts[0]
+ fileName := filepath.Join(parts[1:]...)
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ loaded := false
+ if ok {
+ _, loaded = m.itemByURL[search.URL]
+ }
+ m.mu.RUnlock()
+ if !ok || !loaded {
+ return nil, fs.ErrNotExist
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ for _, a := range assets {
+ if a.Name == fileName {
+ return &macGardenFileInfo{name: a.Name, size: a.Size, mode: 0o444, modTime: time.Now().UTC()}, nil
+ }
+ }
+ }
+
+ return nil, fs.ErrNotExist
+}
+
+func (m *MacGardenFileSystem) DiskUsage(_ string) (totalBytes uint64, freeBytes uint64, err error) {
+ return 0x20000000, 0x18000000, nil
+}
+
+func (m *MacGardenFileSystem) ChildCount(path string) (uint16, error) {
+ rel, err := m.normalize(path)
+ if err != nil {
+ return 0, err
+ }
+ if rel == "" {
+ return 3, nil // Apps + Games + search
+ }
+
+ m.loadCategories()
+ parts := strings.Split(rel, "/")
+ if len(parts) == 1 {
+ switch parts[0] {
+ case "Apps":
+ return m.countCategoriesWithPrefix("/apps/"), nil
+ case "Games":
+ return m.countCategoriesWithPrefix("/games/"), nil
+ }
+ }
+ if len(parts) == 2 && (parts[0] == "Apps" || parts[0] == "Games") {
+ catURL := m.getCategoryURL(parts[1])
+ if catURL == "" {
+ return 0, nil
+ }
+ m.mu.RLock()
+ if count, ok := m.categoryItemCount[catURL]; ok {
+ m.mu.RUnlock()
+ return count, nil
+ }
+ m.mu.RUnlock()
+ // Category counts must remain fully lazy. Until a category has actually
+ // been opened and its items fetched, report an unknown count as zero
+ // rather than triggering remote requests during parent directory enumerate.
+ return 0, nil
+ }
+ if len(parts) == 3 && (parts[0] == "Apps" || parts[0] == "Games") {
+ itemName := parts[2]
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[itemName]
+ item := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if item == nil {
+ return 0, nil
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return 0, nil
+ }
+ return uint16(len(buildItemDirEntries(assets, ""))), nil
+ }
+ if len(parts) >= 4 && (parts[0] == "Apps" || parts[0] == "Games") {
+ itemName := parts[2]
+ subPath := strings.Join(parts[3:], "/")
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[itemName]
+ item := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if item == nil {
+ return 0, nil
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return 0, nil
+ }
+ return uint16(len(buildItemDirEntries(assets, subPath))), nil
+ }
+ if len(parts) >= 1 && parts[0] == "search" {
+ switch len(parts) {
+ case 1:
+ // /search — number of cached queries.
+ m.mu.RLock()
+ n := uint16(len(m.catSearchCache))
+ m.mu.RUnlock()
+ return n, nil
+ case 2:
+ // /search/ — count distinct type dirs + untyped items.
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if !ok {
+ return 0, nil
+ }
+ typesSeen := map[string]struct{}{}
+ untypedSeen := map[string]struct{}{}
+ for _, page := range cache.pages {
+ for _, r := range page {
+ if r.Type != "" {
+ typesSeen[r.Type] = struct{}{}
+ } else if name := sanitizeGardenName(r.Name); name != "" {
+ untypedSeen[name] = struct{}{}
+ }
+ }
+ }
+ return clampGardenCount(len(typesSeen) + len(untypedSeen)), nil
+ case 3:
+ // /search// — count items of that type.
+ if isSearchResultType(parts[2]) {
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[parts[1]]
+ m.mu.RUnlock()
+ if !ok {
+ return 0, nil
+ }
+ seen := map[string]struct{}{}
+ for _, page := range cache.pages {
+ for _, r := range page {
+ if r.Type == parts[2] {
+ if name := sanitizeGardenName(r.Name); name != "" {
+ seen[name] = struct{}{}
+ }
+ }
+ }
+ }
+ return clampGardenCount(len(seen)), nil
+ }
+ // /search//
- — offspring count for item root.
+ itemName := parts[2]
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[itemName]
+ item := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if item == nil {
+ return 0, nil
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return 0, nil
+ }
+ return uint16(len(buildItemDirEntries(assets, ""))), nil
+ default:
+ // /search///
- [/] or /search//
- /
+ var itemName, subPath string
+ if isSearchResultType(parts[2]) {
+ itemName = parts[3]
+ subPath = strings.Join(parts[4:], "/")
+ } else {
+ itemName = parts[2]
+ subPath = strings.Join(parts[3:], "/")
+ }
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[itemName]
+ item := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if item == nil {
+ return 0, nil
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return 0, nil
+ }
+ return uint16(len(buildItemDirEntries(assets, subPath))), nil
+ }
+ }
+ if len(parts) == 1 {
+ return 0, nil
+ }
+ return 0, newNotSupported("ChildCount")
+}
+
+// DirAttributes returns AFP directory attribute bits for a path.
+// /search is flagged invisible so it stays hidden from normal Finder browsing.
+func (m *MacGardenFileSystem) DirAttributes(path string) (uint16, error) {
+ rel, err := m.normalize(path)
+ if err != nil {
+ return 0, err
+ }
+ if rel == "search" {
+ return DirAttrInvisible, nil
+ }
+ return 0, nil
+}
+
+func (m *MacGardenFileSystem) IsReadOnly(_ string) (bool, error) {
+ return true, nil
+}
+
+// SetMaxRangeSize limits each HTTP range request to at most n bytes.
+// Called by the AFP service with the ASP quantum size so that reads from
+// macintoshgarden.org never exceed what can fit in one ASP reply.
+func (m *MacGardenFileSystem) SetMaxRangeSize(n int) {
+ m.client.SetMaxRangeSize(n)
+}
+
+func (m *MacGardenFileSystem) SupportsCatSearch(_ string) (bool, error) {
+ return true, nil
+}
+
+func (m *MacGardenFileSystem) Capabilities() FileSystemCapabilities {
+ return FileSystemCapabilities{
+ CatSearch: true,
+ ChildCount: true,
+ ReadDirRange: true,
+ DirAttributes: true,
+ ReadOnlyState: true,
+ }
+}
+
+func (m *MacGardenFileSystem) Close() error {
+ m.stopOnce.Do(func() { close(m.stop) })
+ m.wg.Wait()
+ return nil
+}
+
+func (m *MacGardenFileSystem) CreateDir(_ string) error { return fs.ErrPermission }
+func (m *MacGardenFileSystem) CreateFile(_ string) (File, error) { return nil, fs.ErrPermission }
+func (m *MacGardenFileSystem) Remove(_ string) error { return fs.ErrPermission }
+func (m *MacGardenFileSystem) Rename(_, _ string) error { return fs.ErrPermission }
+
+// openAsset wraps an asset in a macGardenFile, populating Content from the
+// in-memory screenshot cache when the image has already been downloaded.
+func (m *MacGardenFileSystem) openAsset(a macGardenAsset) *macGardenFile {
+ if strings.HasPrefix(a.Name, "Screenshots/") && a.URL != "" && len(a.Content) == 0 {
+ m.screenshotMu.RLock()
+ data, ok := m.screenshotCache[a.URL]
+ m.screenshotMu.RUnlock()
+ if ok {
+ a.Content = data
+ a.Size = int64(len(data))
+ }
+ }
+ return &macGardenFile{asset: a, client: m.client}
+}
+
+func (m *MacGardenFileSystem) OpenFile(path string, flag int) (File, error) {
+ if flag&(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 {
+ return nil, fs.ErrPermission
+ }
+ rel, err := m.normalize(path)
+ if err != nil {
+ return nil, err
+ }
+
+ parts := strings.Split(rel, "/")
+
+ // /search//[/]
- /
+ if len(parts) >= 4 && parts[0] == "search" {
+ var itemName, fileName string
+ if isSearchResultType(parts[2]) {
+ if len(parts) < 5 {
+ return nil, fs.ErrInvalid
+ }
+ itemName = parts[3]
+ fileName = strings.Join(parts[4:], "/")
+ } else {
+ itemName = parts[2]
+ fileName = strings.Join(parts[3:], "/")
+ }
+ m.mu.RLock()
+ search, ok := m.searchByName[itemName]
+ m.mu.RUnlock()
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ if err := m.ensureItemForDir(itemName, search.URL); err != nil {
+ return nil, fs.ErrNotExist
+ }
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+ for _, a := range assets {
+ if a.Name == fileName {
+ return m.openAsset(a), nil
+ }
+ }
+ return nil, fs.ErrNotExist
+ }
+
+ // Must be asset level: Apps/Category/Item/Asset or deeper
+ if len(parts) < 4 || (parts[0] != "Apps" && parts[0] != "Games") {
+ return nil, fs.ErrInvalid
+ }
+
+ catName := parts[1]
+ itemName := parts[2]
+ fileName := strings.Join(parts[3:], "/")
+
+ catURL := m.getCategoryURL(catName)
+ if catURL == "" {
+ return nil, fs.ErrNotExist
+ }
+
+ itemURL, err := m.getItemURLInCategory(catURL, itemName)
+ if err != nil {
+ return nil, fs.ErrNotExist
+ }
+
+ if err := m.ensureItemForDir(itemName, itemURL); err != nil {
+ return nil, fs.ErrNotExist
+ }
+
+ assets, err := m.itemAssetsByDir(itemName)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, a := range assets {
+ if a.Name == fileName {
+ return m.openAsset(a), nil
+ }
+ }
+ return nil, fs.ErrNotExist
+}
+
+func (m *MacGardenFileSystem) CatSearch(_ string, query string, reqMatches int32, cursor [16]byte) ([]string, [16]byte, int32) {
+ rawQuery := strings.TrimSpace(query)
+ if rawQuery == "" {
+ return nil, cursor, ErrParamErr
+ }
+ normalizedQuery := normalizeMacGardenSearchQuery(rawQuery)
+ if normalizedQuery == "" {
+ return nil, cursor, ErrParamErr
+ }
+
+ limit := int(reqMatches)
+ if limit <= 0 {
+ limit = 25
+ }
+
+ isContinuation := cursor[0] == 0x01
+ cursorQueryHash := uint32(cursor[1])<<16 | uint32(cursor[2])<<8 | uint32(cursor[3])
+ cursorOffset := uint32(cursor[4])<<24 | uint32(cursor[5])<<16 | uint32(cursor[6])<<8 | uint32(cursor[7])
+
+ queryHash := uint32(0)
+ if len(normalizedQuery) >= 3 {
+ queryHash = uint32(normalizedQuery[0])<<16 | uint32(normalizedQuery[1])<<8 | uint32(normalizedQuery[2])
+ } else if len(normalizedQuery) > 0 {
+ for i := 0; i < len(normalizedQuery); i++ {
+ queryHash = (queryHash << 8) | uint32(normalizedQuery[i])
+ }
+ }
+
+ startIdx := 0
+ if isContinuation && cursorQueryHash == queryHash {
+ startIdx = int(cursorOffset)
+ } else {
+ netlog.Debug("[MacGarden][CatSearch] starting new search for %q", normalizedQuery)
+ }
+
+ // Determine which page startIdx falls on and skip to the right entry within it.
+ firstPage := startIdx / macGardenSearchPageSize
+ skipInFirst := startIdx % macGardenSearchPageSize
+
+ type hit struct {
+ result garden.SearchResult
+ name string
+ }
+ hits := make([]hit, 0, limit)
+ exhausted := false
+
+ for pageNum := firstPage; len(hits) < limit; pageNum++ {
+ m.ensureSearchPage(normalizedQuery, pageNum)
+
+ m.mu.RLock()
+ cache := m.catSearchCache[normalizedQuery]
+ var page []garden.SearchResult
+ if cache != nil {
+ page = cache.pages[pageNum]
+ exhausted = cache.exhausted
+ }
+ m.mu.RUnlock()
+
+ if len(page) == 0 {
+ break
+ }
+
+ skip := 0
+ if pageNum == firstPage {
+ skip = skipInFirst
+ }
+ for i := skip; i < len(page) && len(hits) < limit; i++ {
+ name := sanitizeGardenName(page[i].Name)
+ if name != "" {
+ hits = append(hits, hit{result: page[i], name: name})
+ }
+ }
+
+ if len(page) < macGardenSearchPageSize || exhausted {
+ break
+ }
+ }
+
+ netlog.Debug("[MacGarden][CatSearch] query=%q startIdx=%d firstPage=%d skip=%d returned=%d exhausted=%v",
+ normalizedQuery, startIdx, firstPage, skipInFirst, len(hits), exhausted)
+
+ paths := make([]string, 0, len(hits))
+ m.mu.Lock()
+ for _, h := range hits {
+ dir := h.name
+ if h.result.Type != "" {
+ dir = filepath.Join(h.result.Type, h.name)
+ }
+ paths = append(paths, filepath.Join(m.root, "search", normalizedQuery, dir))
+ m.searchByName[h.name] = macGardenCachedResult{Name: h.result.Name, URL: h.result.URL}
+ m.itemURLByDir[h.name] = h.result.URL
+ }
+ m.mu.Unlock()
+
+ moreAvailable := len(hits) == limit || !exhausted
+
+ nextCursor := [16]byte{}
+ nextCursor[1] = byte((queryHash >> 16) & 0xFF)
+ nextCursor[2] = byte((queryHash >> 8) & 0xFF)
+ nextCursor[3] = byte(queryHash & 0xFF)
+ if moreAvailable {
+ nextCursor[0] = 0x01
+ nextOffset := uint32(startIdx + len(hits))
+ nextCursor[4] = byte((nextOffset >> 24) & 0xFF)
+ nextCursor[5] = byte((nextOffset >> 16) & 0xFF)
+ nextCursor[6] = byte((nextOffset >> 8) & 0xFF)
+ nextCursor[7] = byte(nextOffset & 0xFF)
+ }
+
+ return paths, nextCursor, NoErr
+}
+
+// ensureSearchPage fetches a single MacGarden search page into the cache if it
+// is not already there. Marks the cache exhausted when the page is partial
+// (fewer than macGardenSearchPageSize items) or returns an error.
+func (m *MacGardenFileSystem) ensureSearchPage(normalizedQuery string, pageNum int) {
+ m.mu.RLock()
+ cache, ok := m.catSearchCache[normalizedQuery]
+ if ok {
+ if _, cached := cache.pages[pageNum]; cached {
+ m.mu.RUnlock()
+ return
+ }
+ if cache.exhausted {
+ m.mu.RUnlock()
+ return
+ }
+ }
+ m.mu.RUnlock()
+
+ netlog.Debug("[MacGarden][CatSearch] fetching search page %d for %q", pageNum, normalizedQuery)
+ pageResults, err := m.client.GetSearchPage(normalizedQuery, pageNum)
+
+ m.mu.Lock()
+ cache, ok = m.catSearchCache[normalizedQuery]
+ if !ok {
+ cache = &macGardenSearchCache{pages: make(map[int][]garden.SearchResult)}
+ }
+ if _, alreadyCached := cache.pages[pageNum]; !alreadyCached {
+ if err != nil {
+ netlog.Warn("[MacGarden][CatSearch] page %d fetch failed for %q: %v", pageNum, normalizedQuery, err)
+ cache.exhausted = true
+ } else {
+ cache.pages[pageNum] = pageResults
+ if len(pageResults) < macGardenSearchPageSize {
+ netlog.Debug("[MacGarden][CatSearch] page %d: %d results for %q (last page)", pageNum, len(pageResults), normalizedQuery)
+ cache.exhausted = true
+ } else {
+ netlog.Debug("[MacGarden][CatSearch] page %d: %d results for %q", pageNum, len(pageResults), normalizedQuery)
+ }
+ }
+ m.catSearchCache[normalizedQuery] = cache
+ }
+ m.mu.Unlock()
+}
+
+func normalizeMacGardenSearchQuery(s string) string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return ""
+ }
+ lower := strings.ToLower(s)
+ for _, marker := range []string{" type:app,game", " type:app", " type:game", "type:app,game", "type:app", "type:game"} {
+ if idx := strings.Index(lower, marker); idx >= 0 {
+ s = s[:idx]
+ lower = strings.ToLower(s)
+ }
+ }
+ quoted := extractQuotedSegments(s)
+ if len(quoted) > 0 {
+ best := ""
+ bestScore := -1
+ for _, q := range quoted {
+ cand := cleanMacGardenCandidate(q)
+ score := 0
+ for _, r := range cand {
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
+ score++
+ }
+ }
+ if score > bestScore {
+ bestScore = score
+ best = cand
+ }
+ }
+ if best != "" {
+ return best
+ }
+ }
+ return cleanMacGardenCandidate(s)
+}
+
+func mirrorFolderForURL(rawURL string) string {
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return "mirror-unknown"
+ }
+ switch strings.ToLower(u.Host) {
+ case "old.mac.gdn":
+ return "mirror-old"
+ case "download.macintoshgarden.org":
+ return "mirror-download"
+ default:
+ return "mirror-unknown"
+ }
+}
+
+func buildItemDirEntries(assets []macGardenAsset, subPath string) []fs.DirEntry {
+ subPath = strings.Trim(strings.ReplaceAll(subPath, "\\", "/"), "/")
+ dirSeen := make(map[string]struct{})
+ fileSeen := make(map[string]struct{})
+ entries := make([]fs.DirEntry, 0, len(assets))
+
+ for _, a := range assets {
+ name := strings.Trim(strings.ReplaceAll(a.Name, "\\", "/"), "/")
+ if name == "" {
+ continue
+ }
+ if subPath != "" {
+ prefix := subPath + "/"
+ if !strings.HasPrefix(name, prefix) {
+ continue
+ }
+ name = strings.TrimPrefix(name, prefix)
+ if name == "" {
+ continue
+ }
+ }
+
+ if idx := strings.Index(name, "/"); idx >= 0 {
+ dirName := name[:idx]
+ if dirName == "" {
+ continue
+ }
+ if _, ok := dirSeen[dirName]; ok {
+ continue
+ }
+ dirSeen[dirName] = struct{}{}
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: dirName, mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ continue
+ }
+
+ if _, ok := fileSeen[name]; ok {
+ continue
+ }
+ fileSeen[name] = struct{}{}
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: name, size: a.Size, mode: 0o444, modTime: time.Now().UTC()}})
+ }
+
+ sort.Slice(entries, func(i, j int) bool {
+ return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name())
+ })
+ return entries
+}
+
+func cleanMacGardenCandidate(s string) string {
+ s = strings.NewReplacer("$", "", "@", " ", "\"", " ").Replace(s)
+ s = strings.TrimSpace(s)
+ s = strings.Trim(s, ".,:;()[]{}<>' ")
+ s = strings.Join(strings.Fields(s), " ")
+ if s == "" || s == "." {
+ return ""
+ }
+ return s
+}
+
+func extractQuotedSegments(s string) []string {
+ segments := make([]string, 0, 2)
+ start := -1
+ for i, r := range s {
+ if r != '"' {
+ continue
+ }
+ if start < 0 {
+ start = i + 1
+ continue
+ }
+ if start <= i {
+ segments = append(segments, s[start:i])
+ }
+ start = -1
+ }
+ return segments
+}
+
+func (m *MacGardenFileSystem) ensureItemForDir(dirName string, fallbackURL string) error {
+ dirName = strings.TrimSpace(pathBase(dirName))
+ if dirName == "" {
+ return fs.ErrNotExist
+ }
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[dirName]
+ m.mu.RUnlock()
+ if itemURL == "" {
+ itemURL = fallbackURL
+ }
+ if itemURL == "" {
+ return fs.ErrNotExist
+ }
+
+ m.mu.RLock()
+ _, ok := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if ok {
+ return nil
+ }
+
+ item, err := m.client.GetSoftwareItem(itemURL)
+ if err != nil {
+ return err
+ }
+ m.mu.Lock()
+ m.itemByURL[itemURL] = item
+ m.itemURLByDir[dirName] = itemURL
+ m.mu.Unlock()
+ return nil
+}
+
+func (m *MacGardenFileSystem) itemAssetsByDir(dirName string) ([]macGardenAsset, error) {
+ dirName = pathBase(dirName)
+ m.mu.RLock()
+ itemURL := m.itemURLByDir[dirName]
+ item := m.itemByURL[itemURL]
+ m.mu.RUnlock()
+ if itemURL == "" || item == nil {
+ return nil, fs.ErrNotExist
+ }
+
+ netlog.Info("[AFP][MacGarden] building assets for %q: %d screenshot(s), %d download group(s)", dirName, len(item.Screenshots), len(item.Downloads))
+ assets := make([]macGardenAsset, 0, len(item.Downloads)+len(item.Screenshots)+2)
+ txtPath := filepath.Join(dirName, "Description.txt")
+ htmlPath := filepath.Join(dirName, "Description.html")
+ descMac := strings.ReplaceAll(item.Description, "\n", "\r")
+ txtBytes := []byte(descMac)
+ htmlBytes := []byte("
" + htmlEscape(item.Description) + "
")
+ assets = append(assets,
+ macGardenAsset{Name: "Description.txt", Content: txtBytes, Size: int64(len(txtBytes))},
+ macGardenAsset{Name: "Description.html", Content: htmlBytes, Size: int64(len(htmlBytes))},
+ )
+
+ m.mu.Lock()
+ m.descriptionByPath[txtPath] = assets[0]
+ m.descriptionByPath[htmlPath] = assets[1]
+ m.mu.Unlock()
+
+ // For each URL use the cached size if available; collect uncached URLs for
+ // background probing so this function never blocks on network I/O.
+ var needsProbe []string
+
+ shotIdx := 1
+ for _, shotURL := range item.Screenshots {
+ if !strings.HasPrefix(shotURL, "http://") && !strings.HasPrefix(shotURL, "https://") {
+ continue
+ }
+ name := fmt.Sprintf("Screenshots/Screenshot %02d %s", shotIdx, garden.FileNameFromURL(shotURL, "image"))
+ size, cached := m.client.CachedContentLength(shotURL)
+ if !cached {
+ netlog.Debug("[AFP][MacGarden] screenshot %d/%d not yet cached, will probe in background", shotIdx, len(item.Screenshots))
+ needsProbe = append(needsProbe, shotURL)
+ } else {
+ netlog.Debug("[AFP][MacGarden] screenshot %d size: %d bytes (cached)", shotIdx, size)
+ }
+ asset := macGardenAsset{Name: name, URL: shotURL, Size: size}
+ assets = append(assets, asset)
+ m.mu.Lock()
+ m.screenshotByPath[filepath.Join(dirName, name)] = asset
+ m.mu.Unlock()
+ shotIdx++
+ }
+
+ for _, dl := range item.Downloads {
+ for _, link := range dl.Links {
+ if !strings.HasPrefix(link.URL, "http://") && !strings.HasPrefix(link.URL, "https://") {
+ continue
+ }
+ // Skip MD5 checksum links — they are not downloadable files.
+ if strings.Contains(link.URL, "arch_md5.php") {
+ continue
+ }
+ base := garden.FileNameFromURL(link.URL, dl.Title)
+ if base == "" {
+ base = sanitizeGardenName(dl.Title)
+ }
+ name := mirrorFolderForURL(link.URL) + "/" + base
+ size, cached := m.client.CachedContentLength(link.URL)
+ if !cached {
+ netlog.Debug("[AFP][MacGarden] download %q not yet cached, will probe in background", dl.Title)
+ needsProbe = append(needsProbe, link.URL)
+ } else {
+ netlog.Debug("[AFP][MacGarden] download %q size: %d bytes (cached)", dl.Title, size)
+ }
+ asset := macGardenAsset{Name: name, URL: link.URL, Size: size}
+ assets = append(assets, asset)
+ m.mu.Lock()
+ m.downloadByPath[filepath.Join(dirName, name)] = asset
+ m.mu.Unlock()
+ }
+ }
+
+ if len(needsProbe) > 0 && m.client.FetchHead() {
+ netlog.Info("[AFP][MacGarden] probing %d uncached asset size(s) for %q in background", len(needsProbe), dirName)
+ urls := needsProbe
+ m.wg.Add(1)
+ go func() {
+ defer m.wg.Done()
+ for _, u := range urls {
+ select {
+ case <-m.stop:
+ return
+ default:
+ }
+ if _, err := m.client.HeadContentLength(u); err != nil {
+ netlog.Warn("[AFP][MacGarden] background probe failed for %q: %v", u, err)
+ }
+ }
+ netlog.Info("[AFP][MacGarden] background probe complete for %q", dirName)
+ }()
+ }
+
+ netlog.Info("[AFP][MacGarden] built %d asset(s) for %q", len(assets), dirName)
+ return assets, nil
+}
+
+func (m *MacGardenFileSystem) categoryByName(name string) (garden.Category, bool) {
+ for _, c := range m.categories {
+ if c.Name == name {
+ return c, true
+ }
+ }
+ return garden.Category{}, false
+}
+
+func (m *MacGardenFileSystem) getCategoryURL(catName string) string {
+ m.loadCategories()
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ for _, c := range m.categories {
+ if c.Name == catName {
+ return c.URL
+ }
+ }
+ return ""
+}
+
+func (m *MacGardenFileSystem) getCategoryPageMeta(catURL string) (macGardenCategoryPageMeta, error) {
+ m.mu.RLock()
+ if meta, ok := m.categoryPageMeta[catURL]; ok {
+ m.mu.RUnlock()
+ return meta, nil
+ }
+ m.mu.RUnlock()
+
+ info, err := m.client.GetCategoryPageInfo(catURL)
+ if err != nil {
+ return macGardenCategoryPageMeta{}, err
+ }
+ meta := macGardenCategoryPageMeta{
+ TotalCount: clampGardenCount(info.TotalCount),
+ PageSize: info.PageSize,
+ LastPageNumber: info.LastPageNumber,
+ LastPageCount: info.LastPageCount,
+ }
+ m.mu.Lock()
+ m.categoryPageMeta[catURL] = meta
+ m.categoryItemCount[catURL] = meta.TotalCount
+ m.cacheCategoryPageLocked(catURL, 0, info.FirstPage)
+ if info.LastPageNumber > 0 {
+ m.cacheCategoryPageLocked(catURL, info.LastPageNumber, info.LastPage)
+ }
+ m.mu.Unlock()
+ return meta, nil
+}
+
+func (m *MacGardenFileSystem) getCategoryPage(catURL string, pageNumber int) ([]garden.SearchResult, error) {
+ m.mu.RLock()
+ if pages, ok := m.categoryPageItems[catURL]; ok {
+ if items, ok := pages[pageNumber]; ok {
+ cached := append([]garden.SearchResult(nil), items...)
+ m.mu.RUnlock()
+ return cached, nil
+ }
+ }
+ m.mu.RUnlock()
+
+ items, err := m.client.GetCategoryPage(catURL, pageNumber)
+ if err != nil {
+ return nil, err
+ }
+ m.mu.Lock()
+ m.cacheCategoryPageLocked(catURL, pageNumber, items)
+ m.mu.Unlock()
+ return append([]garden.SearchResult(nil), items...), nil
+}
+
+func (m *MacGardenFileSystem) cacheCategoryPageLocked(catURL string, pageNumber int, items []garden.SearchResult) {
+ if _, ok := m.categoryPageItems[catURL]; !ok {
+ m.categoryPageItems[catURL] = make(map[int][]garden.SearchResult)
+ }
+ cloned := append([]garden.SearchResult(nil), items...)
+ m.categoryPageItems[catURL][pageNumber] = cloned
+ for _, item := range cloned {
+ name := sanitizeGardenName(item.Name)
+ if name == "" {
+ continue
+ }
+ m.itemURLByDir[name] = item.URL
+ }
+}
+
+func (m *MacGardenFileSystem) readCategoryDirRange(catURL string, startIndex uint16, reqCount uint16) ([]fs.DirEntry, uint16, error) {
+ if reqCount > macGardenEnumerateWindow {
+ reqCount = macGardenEnumerateWindow
+ }
+ meta, err := m.getCategoryPageMeta(catURL)
+ if err != nil {
+ return nil, 0, err
+ }
+ total := meta.TotalCount
+ if total == 0 {
+ return nil, 0, nil
+ }
+ if startIndex < 1 {
+ startIndex = 1
+ }
+ if startIndex > total {
+ return nil, total, nil
+ }
+ if reqCount == 0 {
+ return nil, total, nil
+ }
+ pageSize := meta.PageSize
+ if pageSize <= 0 {
+ return nil, total, nil
+ }
+ startOffset := int(startIndex) - 1
+ endOffset := startOffset + int(reqCount)
+ if endOffset > int(total) {
+ endOffset = int(total)
+ }
+ firstPage := startOffset / pageSize
+ lastPage := (endOffset - 1) / pageSize
+ results := make([]garden.SearchResult, 0, endOffset-startOffset)
+ for pageNumber := firstPage; pageNumber <= lastPage; pageNumber++ {
+ items, err := m.getCategoryPage(catURL, pageNumber)
+ if err != nil {
+ return nil, total, err
+ }
+ pageStart := 0
+ if pageNumber == firstPage {
+ pageStart = startOffset - pageNumber*pageSize
+ }
+ pageEnd := len(items)
+ if pageNumber == lastPage {
+ pageLimit := endOffset - pageNumber*pageSize
+ if pageLimit < pageEnd {
+ pageEnd = pageLimit
+ }
+ }
+ if pageStart < 0 {
+ pageStart = 0
+ }
+ if pageStart > len(items) {
+ pageStart = len(items)
+ }
+ if pageEnd < pageStart {
+ pageEnd = pageStart
+ }
+ results = append(results, items[pageStart:pageEnd]...)
+ }
+ entries := make([]fs.DirEntry, 0, len(results))
+ for _, item := range results {
+ entries = append(entries, macGardenDirEntry{info: &macGardenFileInfo{name: sanitizeGardenName(item.Name), mode: fs.ModeDir | 0o555, isDir: true, modTime: time.Now().UTC()}})
+ }
+ return entries, total, nil
+}
+
+func (m *MacGardenFileSystem) getCategoryItems(catURL string) ([]garden.SearchResult, error) {
+ netlog.Debug("[AFP][MacGarden] getCategoryItems for URL: %s", catURL)
+ m.mu.RLock()
+ if items, ok := m.itemsInCategory[catURL]; ok {
+ m.mu.RUnlock()
+ netlog.Debug("[AFP][MacGarden] getCategoryItems found %d cached items for %s", len(items), catURL)
+ return items, nil
+ }
+ m.mu.RUnlock()
+
+ meta, err := m.getCategoryPageMeta(catURL)
+ if err != nil {
+ netlog.Warn("[AFP][MacGarden] failed to fetch category page metadata: %v", err)
+ return nil, err
+ }
+
+ netlog.Debug("[AFP][MacGarden] fetching all pages for category URL: %s", catURL)
+ items := make([]garden.SearchResult, 0, int(meta.TotalCount))
+ for pageNumber := 0; pageNumber <= meta.LastPageNumber; pageNumber++ {
+ pageItems, err := m.getCategoryPage(catURL, pageNumber)
+ if err != nil {
+ netlog.Warn("[AFP][MacGarden] failed to fetch category page %d: %v", pageNumber, err)
+ return nil, err
+ }
+ items = append(items, pageItems...)
+ }
+
+ netlog.Info("[AFP][MacGarden] got %d items from category %s", len(items), catURL)
+ m.mu.Lock()
+ m.itemsInCategory[catURL] = items
+ m.categoryItemCount[catURL] = clampGardenCount(len(items))
+ m.mu.Unlock()
+ return items, nil
+}
+
+func clampGardenCount(count int) uint16 {
+ if count <= 0 {
+ return 0
+ }
+ if count > 0xffff {
+ return 0xffff
+ }
+ return uint16(count)
+}
+
+func (m *MacGardenFileSystem) countCategoriesWithPrefix(prefix string) uint16 {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ count := uint16(0)
+ for _, cat := range m.categories {
+ if strings.HasPrefix(strings.ToLower(urlPathFromAbsolute(cat.URL)), prefix) {
+ count++
+ }
+ }
+ return count
+}
+
+func (m *MacGardenFileSystem) getItemURLInCategory(catURL string, itemName string) (string, error) {
+ // Fast path: if the item URL is already cached from prior ranged enumeration,
+ // avoid forcing a full category crawl.
+ m.mu.RLock()
+ if cachedURL := m.itemURLByDir[itemName]; cachedURL != "" {
+ m.mu.RUnlock()
+ return cachedURL, nil
+ }
+ if cachedItems, ok := m.itemsInCategory[catURL]; ok {
+ for _, item := range cachedItems {
+ if sanitizeGardenName(item.Name) == itemName {
+ m.mu.RUnlock()
+ return item.URL, nil
+ }
+ }
+ }
+ if cachedPages, ok := m.categoryPageItems[catURL]; ok {
+ for _, pageItems := range cachedPages {
+ for _, item := range pageItems {
+ if sanitizeGardenName(item.Name) == itemName {
+ m.mu.RUnlock()
+ return item.URL, nil
+ }
+ }
+ }
+ }
+ m.mu.RUnlock()
+
+ meta, err := m.getCategoryPageMeta(catURL)
+ if err != nil {
+ return "", err
+ }
+
+ for pageNumber := 0; pageNumber <= meta.LastPageNumber; pageNumber++ {
+ pageItems, err := m.getCategoryPage(catURL, pageNumber)
+ if err != nil {
+ return "", err
+ }
+ for _, item := range pageItems {
+ if sanitizeGardenName(item.Name) == itemName {
+ return item.URL, nil
+ }
+ }
+ }
+ return "", fs.ErrNotExist
+}
+
+func isSearchResultType(s string) bool { return s == "App" || s == "Game" }
+
+func sanitizeGardenName(s string) string {
+ s = strings.TrimSpace(s)
+ replacer := strings.NewReplacer(
+ "\\", "_",
+ "/", "_",
+ ":", "-",
+ "*", "_",
+ "?", "",
+ "\"", "",
+ "<", "(",
+ ">", ")",
+ "|", "_",
+ )
+ s = replacer.Replace(s)
+ if s == "" {
+ return "Item"
+ }
+ return s
+}
+
+func htmlEscape(s string) string {
+ s = strings.ReplaceAll(s, "&", "&")
+ s = strings.ReplaceAll(s, "<", "<")
+ s = strings.ReplaceAll(s, ">", ">")
+ return s
+}
+
+func pathBase(s string) string {
+ s = filepath.ToSlash(s)
+ parts := strings.Split(s, "/")
+ return parts[len(parts)-1]
+}
+
+func pathDir(s string) string {
+ s = filepath.ToSlash(s)
+ idx := strings.LastIndex(s, "/")
+ if idx < 0 {
+ return ""
+ }
+ return s[:idx]
+}
+
+func urlPathFromAbsolute(absURL string) string {
+ u, err := url.Parse(absURL)
+ if err != nil {
+ return ""
+ }
+ return u.Path
+}
+
+var _ FileSystem = (*MacGardenFileSystem)(nil)
+
+var errMacGardenNotFound = errors.New("macgarden: not found")
diff --git a/service/afp/metadata.go b/service/afp/metadata.go
index ee0c883..cdda9f9 100644
--- a/service/afp/metadata.go
+++ b/service/afp/metadata.go
@@ -8,8 +8,8 @@ import (
"path/filepath"
"strings"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/cnid"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/cnid"
)
// AppleDouble sidecar / hidden-name / icon canonicalisation helpers.
diff --git a/service/afp/metrics.go b/service/afp/metrics.go
index c2f119d..12cbce6 100644
--- a/service/afp/metrics.go
+++ b/service/afp/metrics.go
@@ -2,6 +2,6 @@
package afp
-import "github.com/pgodw/omnitalk/pkg/telemetry"
+import "github.com/ObsoleteMadness/ClassicStack/pkg/telemetry"
-var afpCommandsTotal = telemetry.NewCounter("omnitalk_afp_commands_total")
+var afpCommandsTotal = telemetry.NewCounter("classicstack_afp_commands_total")
diff --git a/service/afp/pascal_string.go b/service/afp/pascal_string.go
index 33a0418..2275065 100644
--- a/service/afp/pascal_string.go
+++ b/service/afp/pascal_string.go
@@ -2,7 +2,7 @@
package afp
-import "github.com/pgodw/omnitalk/pkg/encoding"
+import "github.com/ObsoleteMadness/ClassicStack/pkg/encoding"
// ReadPascalString reads a length-prefixed MacRoman string at idx and returns UTF-8 text plus bytes consumed.
func ReadPascalString(data []byte, idx int) (string, int) {
diff --git a/service/afp/path_codec.go b/service/afp/path_codec.go
index eb090a0..bbfb68d 100644
--- a/service/afp/path_codec.go
+++ b/service/afp/path_codec.go
@@ -9,7 +9,7 @@ import (
"strings"
"unicode/utf8"
- "github.com/pgodw/omnitalk/pkg/encoding"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/encoding"
)
// AFPOptions controls AFP filename/path translation behavior.
diff --git a/service/afp/paths.go b/service/afp/paths.go
index 43a95c8..dfb9632 100644
--- a/service/afp/paths.go
+++ b/service/afp/paths.go
@@ -3,9 +3,10 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"path/filepath"
"strings"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
// CNID-backed path/DID resolution and AFP path-string parsing. The
diff --git a/service/afp/server.go b/service/afp/server.go
index ec2832f..43b1ca4 100644
--- a/service/afp/server.go
+++ b/service/afp/server.go
@@ -17,9 +17,9 @@ import (
"fmt"
"sync"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/protocol/ddp"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// Service implements AppleTalk Filing Protocol.
@@ -109,7 +109,6 @@ func NewService(serverName string, configs []VolumeConfig, fs FileSystem, transp
return s
}
-
// Start initializes all underlying transports and resolves the read-size cap
// from whichever transport advertises the smallest non-zero quantum.
func (s *Service) Start(ctx context.Context, router service.Router) error {
@@ -182,5 +181,3 @@ func (s *Service) Inbound(d ddp.Datagram, p port.Port) {
func (s *Service) GetStatus() []byte {
return BuildServerInfo(s.ServerName)
}
-
-
diff --git a/service/afp/server_calls.go b/service/afp/server_calls.go
index 9d9a3d4..ab1d52a 100644
--- a/service/afp/server_calls.go
+++ b/service/afp/server_calls.go
@@ -3,8 +3,9 @@
package afp
import (
- "github.com/pgodw/omnitalk/netlog"
"time"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
func (s *Service) handleGetSrvrInfo(req *FPGetSrvrInfoReq) (*FPGetSrvrInfoRes, error) {
diff --git a/service/afp/server_models.go b/service/afp/server_models.go
index 9ab6b3e..7573275 100644
--- a/service/afp/server_models.go
+++ b/service/afp/server_models.go
@@ -8,7 +8,7 @@ import (
"fmt"
"strings"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
// FPGetSrvrInfoReq - request to obtain a block of descriptive information
diff --git a/service/afp/server_models_golden_test.go b/service/afp/server_models_golden_test.go
index e3bac0e..243043c 100644
--- a/service/afp/server_models_golden_test.go
+++ b/service/afp/server_models_golden_test.go
@@ -71,7 +71,7 @@ func TestFPMapNameRes_MarshalGolden(t *testing.T) {
// TestFPGetSrvrMsgRes_MarshalGolden pins the wire-format output.
func TestFPGetSrvrMsgRes_MarshalGolden(t *testing.T) {
t.Parallel()
- res := &FPGetSrvrMsgRes{MessageType: 1, Bitmap: 3, Message: "Welcome to OmniTalk"}
+ res := &FPGetSrvrMsgRes{MessageType: 1, Bitmap: 3, Message: "Welcome to ClassicStack"}
got := res.Marshal()
want := goldenBytes(t, "fpgetsrvrmsgres_basic.hex", got)
if !bytes.Equal(got, want) {
@@ -143,7 +143,7 @@ func TestFPLoginRes_MarshalGolden(t *testing.T) {
func TestFPGetSrvrInfoRes_MarshalGolden(t *testing.T) {
t.Parallel()
res := &FPGetSrvrInfoRes{
- MachineType: "OmniTalk",
+ MachineType: "ClassicStack",
AFPVersions: []string{"AFPVersion 1.1", "AFPVersion 2.0", "AFPVersion 2.1"},
UAMs: []string{"No User Authent", "Cleartxt Passwrd"},
ServerName: "Test Server",
diff --git a/service/afp/transport.go b/service/afp/transport.go
index 5da5895..eaf8681 100644
--- a/service/afp/transport.go
+++ b/service/afp/transport.go
@@ -5,10 +5,10 @@ package afp
import (
"context"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// CommandHandler handles decoded AFP commands from transport protocols.
diff --git a/service/afp/volume.go b/service/afp/volume.go
index aec8658..bd6766c 100644
--- a/service/afp/volume.go
+++ b/service/afp/volume.go
@@ -11,8 +11,8 @@ import (
"strings"
"time"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
const (
@@ -92,7 +92,7 @@ func constrainAFPVolumeType(volType uint16) uint16 {
}
func (s *Service) volumeType(_ *Volume) uint16 {
- // OmniTalk exposes hierarchical volumes with CNID-based directory IDs,
+ // ClassicStack exposes hierarchical volumes with CNID-based directory IDs,
// so we advertise Variable Directory ID semantics.
return constrainAFPVolumeType(AFPVolumeTypeFixedDirID)
}
diff --git a/service/afp/volume_models.go b/service/afp/volume_models.go
index c558945..91787ba 100644
--- a/service/afp/volume_models.go
+++ b/service/afp/volume_models.go
@@ -8,7 +8,7 @@ import (
"fmt"
"strings"
- "github.com/pgodw/omnitalk/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
)
func formatVolBitmap(bitmap uint16) string {
diff --git a/service/afpfs/macgarden/fs.go b/service/afpfs/macgarden/fs.go
index 42bd234..efc6215 100644
--- a/service/afpfs/macgarden/fs.go
+++ b/service/afpfs/macgarden/fs.go
@@ -25,9 +25,9 @@ import (
"time"
"unicode"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/service/afp"
- garden "github.com/pgodw/omnitalk/service/macgarden"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
+ garden "github.com/ObsoleteMadness/ClassicStack/service/macgarden"
)
const macGardenEnumerateWindow = 10
@@ -976,10 +976,10 @@ func (m *MacGardenFileSystem) Close() error {
return nil
}
-func (m *MacGardenFileSystem) CreateDir(_ string) error { return fs.ErrPermission }
+func (m *MacGardenFileSystem) CreateDir(_ string) error { return fs.ErrPermission }
func (m *MacGardenFileSystem) CreateFile(_ string) (afp.File, error) { return nil, fs.ErrPermission }
-func (m *MacGardenFileSystem) Remove(_ string) error { return fs.ErrPermission }
-func (m *MacGardenFileSystem) Rename(_, _ string) error { return fs.ErrPermission }
+func (m *MacGardenFileSystem) Remove(_ string) error { return fs.ErrPermission }
+func (m *MacGardenFileSystem) Rename(_, _ string) error { return fs.ErrPermission }
// openAsset wraps an asset in a macGardenFile, populating Content from the
// in-memory screenshot cache when the image has already been downloaded.
diff --git a/service/afpfs/macgarden/fs_test.go b/service/afpfs/macgarden/fs_test.go
index 9ea0b21..faad7fc 100644
--- a/service/afpfs/macgarden/fs_test.go
+++ b/service/afpfs/macgarden/fs_test.go
@@ -7,8 +7,8 @@ import (
"path/filepath"
"testing"
- "github.com/pgodw/omnitalk/service/afp"
- garden "github.com/pgodw/omnitalk/service/macgarden"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
+ garden "github.com/ObsoleteMadness/ClassicStack/service/macgarden"
)
func TestMacGardenChildCount_CategoryIsLazyUntilCached(t *testing.T) {
diff --git a/service/asp/asp.go b/service/asp/asp.go
index 1c1fbc9..88f1f92 100644
--- a/service/asp/asp.go
+++ b/service/asp/asp.go
@@ -1,7 +1,7 @@
//go:build afp || all
/*
-Package asp implements the AppleTalk Session Protocol (ASP) as a omnitalk
+Package asp implements the AppleTalk Session Protocol (ASP) as a classicstack
service. The ATP transaction layer is provided by go/service/atp; this file
is concerned only with ASP semantics — session lifecycle, command/write
dispatch, tickle keep-alives, attentions — and delegates all retry, XO
@@ -18,14 +18,14 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/afp"
- "github.com/pgodw/omnitalk/service/atp"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
+ "github.com/ObsoleteMadness/ClassicStack/service/atp"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
// ServerSocket is the well-known AppleTalk socket for the AFP/ASP server.
diff --git a/service/asp/asp_test.go b/service/asp/asp_test.go
index 3c9ddc9..a8bb8ca 100644
--- a/service/asp/asp_test.go
+++ b/service/asp/asp_test.go
@@ -6,7 +6,7 @@ import (
"encoding/binary"
"testing"
- "github.com/pgodw/omnitalk/service/atp"
+ "github.com/ObsoleteMadness/ClassicStack/service/atp"
)
type stubCommandHandler struct {
diff --git a/service/asp/session.go b/service/asp/session.go
index c1265ea..5f72b6c 100644
--- a/service/asp/session.go
+++ b/service/asp/session.go
@@ -16,8 +16,8 @@ import (
"sync/atomic"
"time"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/service/atp"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/service/atp"
)
// sessionState names the lifecycle of an ASP session. Legal transitions:
diff --git a/service/asp/types.go b/service/asp/types.go
index a03b255..96f523e 100644
--- a/service/asp/types.go
+++ b/service/asp/types.go
@@ -3,7 +3,7 @@
package asp
import (
- pasp "github.com/pgodw/omnitalk/protocol/asp"
+ pasp "github.com/ObsoleteMadness/ClassicStack/protocol/asp"
)
// SPFunction codes.
@@ -66,10 +66,10 @@ type (
// Parse helpers.
var (
- ParseOpenSessPacket = pasp.ParseOpenSessPacket
- ParseCloseSessPacket = pasp.ParseCloseSessPacket
- ParseGetStatusPacket = pasp.ParseGetStatusPacket
- ParseCommandPacket = pasp.ParseCommandPacket
- ParseWritePacket = pasp.ParseWritePacket
- CloseSessReplyUserData = pasp.CloseSessReplyUserData
+ ParseOpenSessPacket = pasp.ParseOpenSessPacket
+ ParseCloseSessPacket = pasp.ParseCloseSessPacket
+ ParseGetStatusPacket = pasp.ParseGetStatusPacket
+ ParseCommandPacket = pasp.ParseCommandPacket
+ ParseWritePacket = pasp.ParseWritePacket
+ CloseSessReplyUserData = pasp.CloseSessReplyUserData
)
diff --git a/service/atp/transaction.go b/service/atp/transaction.go
index 5324382..772afd9 100644
--- a/service/atp/transaction.go
+++ b/service/atp/transaction.go
@@ -17,8 +17,8 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/netlog"
- patp "github.com/pgodw/omnitalk/protocol/atp"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ patp "github.com/ObsoleteMadness/ClassicStack/protocol/atp"
)
// ----- Address / Sender / Clock -------------------------------------------
diff --git a/service/atp/wire.go b/service/atp/wire.go
index 5832a0c..ab889af 100644
--- a/service/atp/wire.go
+++ b/service/atp/wire.go
@@ -7,7 +7,7 @@
package atp
import (
- patp "github.com/pgodw/omnitalk/protocol/atp"
+ patp "github.com/ObsoleteMadness/ClassicStack/protocol/atp"
)
// Header type.
diff --git a/service/dsi/dsi.go b/service/dsi/dsi.go
index 2c92cac..8b3b1c0 100644
--- a/service/dsi/dsi.go
+++ b/service/dsi/dsi.go
@@ -17,13 +17,13 @@ import (
"net"
"sync"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/binutil"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/afp"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/binutil"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/afp"
)
// DSI Command Codes
diff --git a/service/llap/llap.go b/service/llap/llap.go
index 617d2be..ad0b6b3 100644
--- a/service/llap/llap.go
+++ b/service/llap/llap.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/port/localtalk"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
const (
diff --git a/service/llap/llap_test.go b/service/llap/llap_test.go
index 137fd35..7a974d2 100644
--- a/service/llap/llap_test.go
+++ b/service/llap/llap_test.go
@@ -7,10 +7,10 @@ import (
"testing"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port/localtalk"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port/localtalk"
)
func TestDirectedTransmitLogsRetryAndBackoff(t *testing.T) {
diff --git a/service/macgarden/client.go b/service/macgarden/client.go
index d59fade..17d6ffb 100644
--- a/service/macgarden/client.go
+++ b/service/macgarden/client.go
@@ -21,8 +21,8 @@ import (
"sync"
"time"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
"github.com/PuerkitoBio/goquery"
- "github.com/pgodw/omnitalk/netlog"
)
const (
diff --git a/service/macgarden/client_test.go b/service/macgarden/client_test.go
index 9f46862..b502d0a 100644
--- a/service/macgarden/client_test.go
+++ b/service/macgarden/client_test.go
@@ -17,11 +17,11 @@ import (
)
// requireLiveTests skips tests that reach the public Macintosh Garden site
-// unless OMNITALK_LIVE_TESTS=1 is set. CI runners do not run these.
+// unless CLASSICSTACK_LIVE_TESTS=1 is set. CI runners do not run these.
func requireLiveTests(t *testing.T) {
t.Helper()
- if os.Getenv("OMNITALK_LIVE_TESTS") != "1" {
- t.Skip("skipping live macintoshgarden.org test; set OMNITALK_LIVE_TESTS=1 to enable")
+ if os.Getenv("CLASSICSTACK_LIVE_TESTS") != "1" {
+ t.Skip("skipping live macintoshgarden.org test; set CLASSICSTACK_LIVE_TESTS=1 to enable")
}
}
diff --git a/service/macip/dhcp_client.go b/service/macip/dhcp_client.go
index c38e8c8..5e3965d 100644
--- a/service/macip/dhcp_client.go
+++ b/service/macip/dhcp_client.go
@@ -14,9 +14,9 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/pkg/hwaddr"
- "github.com/pgodw/omnitalk/port/nat"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/hwaddr"
+ "github.com/ObsoleteMadness/ClassicStack/port/nat"
)
const (
diff --git a/service/macip/etherlink.go b/service/macip/etherlink.go
index fd2c6c4..7d79ed3 100644
--- a/service/macip/etherlink.go
+++ b/service/macip/etherlink.go
@@ -10,8 +10,8 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
)
const (
diff --git a/service/macip/macip.go b/service/macip/macip.go
index fac9b1d..da0accb 100644
--- a/service/macip/macip.go
+++ b/service/macip/macip.go
@@ -18,14 +18,14 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
-
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/port/nat"
- "github.com/pgodw/omnitalk/port/rawlink"
- "github.com/pgodw/omnitalk/service"
- "github.com/pgodw/omnitalk/service/zip"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/port/nat"
+ "github.com/ObsoleteMadness/ClassicStack/port/rawlink"
+ "github.com/ObsoleteMadness/ClassicStack/service"
+ "github.com/ObsoleteMadness/ClassicStack/service/zip"
)
const (
diff --git a/service/macip/pool.go b/service/macip/pool.go
index 5e4480b..d8134f3 100644
--- a/service/macip/pool.go
+++ b/service/macip/pool.go
@@ -9,7 +9,7 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
const leaseDuration = 5 * time.Minute
diff --git a/service/macip/state.go b/service/macip/state.go
index 7313835..28baa9e 100644
--- a/service/macip/state.go
+++ b/service/macip/state.go
@@ -8,7 +8,7 @@ import (
"os"
"time"
- "github.com/pgodw/omnitalk/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
)
type savedLease struct {
diff --git a/service/rtmp/responding.go b/service/rtmp/responding.go
index ed21c44..80cbd7b 100644
--- a/service/rtmp/responding.go
+++ b/service/rtmp/responding.go
@@ -5,10 +5,10 @@ import (
"encoding/binary"
"sync"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type RespondingService struct {
diff --git a/service/rtmp/routing_table_aging.go b/service/rtmp/routing_table_aging.go
index 271e401..6800b35 100644
--- a/service/rtmp/routing_table_aging.go
+++ b/service/rtmp/routing_table_aging.go
@@ -5,10 +5,10 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type RoutingTableAgingService struct {
diff --git a/service/rtmp/rtmp.go b/service/rtmp/rtmp.go
index efcf724..1d2cba8 100644
--- a/service/rtmp/rtmp.go
+++ b/service/rtmp/rtmp.go
@@ -3,10 +3,10 @@ package rtmp
import (
"encoding/binary"
- "github.com/pgodw/omnitalk/protocol/ddp"
- prtmp "github.com/pgodw/omnitalk/protocol/rtmp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+ prtmp "github.com/ObsoleteMadness/ClassicStack/protocol/rtmp"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// Wire constants re-exported from protocol/rtmp.
diff --git a/service/rtmp/sending.go b/service/rtmp/sending.go
index 87f1628..9a4be4c 100644
--- a/service/rtmp/sending.go
+++ b/service/rtmp/sending.go
@@ -5,10 +5,10 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type SendingService struct {
diff --git a/service/service.go b/service/service.go
index b0306aa..5936b44 100644
--- a/service/service.go
+++ b/service/service.go
@@ -3,9 +3,9 @@ package service
import (
"context"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
+ "github.com/ObsoleteMadness/ClassicStack/port"
)
// Service is the contract every service registered with the router
diff --git a/service/zip/mock_test.go b/service/zip/mock_test.go
index cc19368..f2116d6 100644
--- a/service/zip/mock_test.go
+++ b/service/zip/mock_test.go
@@ -1,6 +1,6 @@
package zip
-import "github.com/pgodw/omnitalk/internal/testutil"
+import "github.com/ObsoleteMadness/ClassicStack/internal/testutil"
// Package-local aliases that let existing tests keep using the lowercase
// names. The real mocks live in internal/testutil so any future package
diff --git a/service/zip/name_information.go b/service/zip/name_information.go
index 51b85b2..b4022e4 100644
--- a/service/zip/name_information.go
+++ b/service/zip/name_information.go
@@ -5,12 +5,12 @@ import (
"context"
"sync"
- "github.com/pgodw/omnitalk/protocol/ddp"
- "github.com/pgodw/omnitalk/protocol/nbp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/nbp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
// NBP wire-format constants are re-exported from protocol/nbp so the
diff --git a/service/zip/name_information_test.go b/service/zip/name_information_test.go
index 4490fe9..6df16a2 100644
--- a/service/zip/name_information_test.go
+++ b/service/zip/name_information_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"time"
- "github.com/pgodw/omnitalk/internal/testutil"
- "github.com/pgodw/omnitalk/protocol/ddp"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/internal/testutil"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
func newMockPort(network uint16, node uint8, shortString string, isExtended bool) *mockPort {
diff --git a/service/zip/responding.go b/service/zip/responding.go
index 03513a7..7c7ceaa 100644
--- a/service/zip/responding.go
+++ b/service/zip/responding.go
@@ -6,12 +6,12 @@ import (
"encoding/binary"
"sync"
- "github.com/pgodw/omnitalk/pkg/encoding"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/pkg/encoding"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/netlog"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/netlog"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type RespondingService struct {
diff --git a/service/zip/sending.go b/service/zip/sending.go
index 542f32a..8edffd9 100644
--- a/service/zip/sending.go
+++ b/service/zip/sending.go
@@ -5,10 +5,10 @@ import (
"sync"
"time"
- "github.com/pgodw/omnitalk/protocol/ddp"
+ "github.com/ObsoleteMadness/ClassicStack/protocol/ddp"
- "github.com/pgodw/omnitalk/port"
- "github.com/pgodw/omnitalk/service"
+ "github.com/ObsoleteMadness/ClassicStack/port"
+ "github.com/ObsoleteMadness/ClassicStack/service"
)
type SendingService struct {
diff --git a/service/zip/zip.go b/service/zip/zip.go
index 5bc1787..81a553d 100644
--- a/service/zip/zip.go
+++ b/service/zip/zip.go
@@ -1,6 +1,6 @@
package zip
-import pzip "github.com/pgodw/omnitalk/protocol/zip"
+import pzip "github.com/ObsoleteMadness/ClassicStack/protocol/zip"
// Wire constants re-exported from protocol/zip.
const (
diff --git a/spec/00-overview.md b/spec/00-overview.md
index 6f5c012..bbabe4d 100644
--- a/spec/00-overview.md
+++ b/spec/00-overview.md
@@ -1,4 +1,4 @@
-# OmniTalk Service Specifications — Overview
+# ClassicStack Service Specifications — Overview
This directory contains implementation-level specifications for each service in the OmniRouter AppleTalk router. These documents are intended to provide sufficient detail for an independent implementor to create a conformant implementation.