diff --git a/CONSUMERS.md b/CONSUMERS.md deleted file mode 100644 index cd0c00f..0000000 --- a/CONSUMERS.md +++ /dev/null @@ -1,34 +0,0 @@ -# Consumers of go-io - -These modules import `dappco.re/go/core/io`: - -- agent -- core -- config -- go-ai -- go-ansible -- go-blockchain -- go-build -- go-cache -- go-container -- go-crypt -- go-forge -- go-html -- go-infra -- go-ml -- go-mlx -- go-netops -- go-p2p -- go-process -- go-rag -- go-ratelimit -- go-scm -- gui -- ide -- lint -- mcp -- php -- ts -- LEM - -**Breaking change risk: 28 consumers.** diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4153cd3 --- /dev/null +++ b/LICENCE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/RFC.md b/RFC.md new file mode 100644 index 0000000..b648015 --- /dev/null +++ b/RFC.md @@ -0,0 +1,163 @@ +# RFC — Windows support for go-io (local Medium) + +**Status:** draft · **Author:** Cladius · **Date:** 2026-05-12 +**Triggered by:** Lethean Desktop (`lthn/desktop`) Windows cross-compile blocked at `dappco.re/go/io/local/medium.go` syscall references. + +## Problem + +`GOOS=windows go build` fails with three error clusters in `go/local/medium.go`: + +``` +medium.go:465-473 syscall.Stat_t / syscall.Lstat [lstat helper] +medium.go:478-480 syscall.S_IFMT / syscall.S_IFLNK [isSymlink helper] +medium.go:482-499 syscall.Readlink [readlink helper] +``` + +The two `syscall.ENOENT` references (lines 121, 248) are **not blockers** — `syscall.ENOENT` is defined on Windows (POSIX errno compatibility) and compiles fine. + +All three failing helpers are private to `package local` and called only via the three internal helpers `lstat()`, `isSymlink()`, `readlink()`. Used in `medium.go:119,127,146` from `walkSymlink()`-style traversal code. + +## Scope + +In: `go/local/medium.go` (the three private helpers and their type contract). + +Out of scope: +- Public API changes (`Medium` interface methods stay identical) +- Symlink **creation** on Windows (the existing code only reads/walks; create paths use `os.Symlink` already) +- Windows reparse points beyond standard symlinks (junction points, mount points) — Phase 2 + +## Constraints + +- `lstat()` currently returns `*syscall.Stat_t` — a Unix-specific struct. The replacement must be cross-platform. +- `isSymlink()` is called as `isSymlink(uint32(info.Mode))` from line 127 — the call-site casts `info.Mode` to `uint32`. The new abstraction must preserve a callable that takes the relevant Mode representation. +- The existing code lives in `package local`. No new sub-package — keep it flat. + +## Design + +Replace the three syscall-leaning helpers with three platform-abstracted ones, splitting Unix-specific implementations behind build tags. Match the precedent already in `dappco.re/go/process` (`pidfile_unix.go` / `pidfile_windows.go`). + +### New abstraction: `linkInfo` + +```go +// linkInfo carries the fields we actually inspect after lstat. +// Replaces the Unix-only *syscall.Stat_t handle so callers stay +// cross-platform. +type linkInfo struct { + IsSymlink bool // true if the entry is a symbolic link + Mode os.FileMode // standard os.FileMode (Unix mode bits + Go's portable mode bits) + Size int64 + ModTime time.Time +} +``` + +Callers in `medium.go:119-148` only check `isSymlink(...)` and the existing flow doesn't read inode/uid/gid/etc — so the wider `syscall.Stat_t` payload isn't load-bearing. If a future caller needs Unix-only fields, add a `Stat *syscall.Stat_t` field on Unix builds (zero on Windows). + +### File layout + +``` +go/local/ +├── medium.go # body unchanged except call sites moving to abstracted helpers +├── medium_link.go # NEW — linkInfo type + signatures + doc +├── medium_link_unix.go # //go:build !windows +└── medium_link_windows.go # //go:build windows +``` + +### Helper signatures + +```go +// in medium_link.go +func lstat(path string) (linkInfo, error) +func readlink(path string) (string, error) +``` + +`isSymlink()` collapses into `linkInfo.IsSymlink` — no separate function needed. + +### Per-platform implementations + +**`medium_link_unix.go`** — `//go:build !windows` + +```go +func lstat(path string) (linkInfo, error) { + var st syscall.Stat_t + if err := syscall.Lstat(path, &st); err != nil { + return linkInfo{}, err + } + return linkInfo{ + IsSymlink: uint32(st.Mode)&syscall.S_IFMT == syscall.S_IFLNK, + Mode: os.FileMode(st.Mode), + Size: st.Size, + ModTime: time.Unix(st.Mtim.Sec, st.Mtim.Nsec), // platform-specific field name; see note + }, nil +} + +func readlink(path string) (string, error) { + // existing buffer-grow loop — call syscall.Readlink directly here +} +``` + +Note: `Stat_t.Mtim` vs `Stat_t.Mtimespec` varies between Linux and Darwin. Existing macOS code in this file already handles that — copy whatever convention is in use. (If unclear, use `os.Lstat()` for the timestamp and skip `syscall.Stat_t` entirely on macOS too — the only reason to keep `Stat_t` is the inode-level mode bits which we don't actually use beyond symlink detection.) + +**`medium_link_windows.go`** — `//go:build windows` + +```go +func lstat(path string) (linkInfo, error) { + fi, err := os.Lstat(path) + if err != nil { + return linkInfo{}, err + } + return linkInfo{ + IsSymlink: fi.Mode()&os.ModeSymlink != 0, + Mode: fi.Mode(), + Size: fi.Size(), + ModTime: fi.ModTime(), + }, nil +} + +func readlink(path string) (string, error) { + return os.Readlink(path) +} +``` + +### Call-site rewrites in `medium.go` + +| Line | Before | After | +|---|---|---| +| 119 | `info, err := lstat(next)` (returns `*syscall.Stat_t`) | `info, err := lstat(next)` (returns `linkInfo`) — call site unchanged | +| 127 | `if !isSymlink(uint32(info.Mode)) {` | `if !info.IsSymlink {` | +| 146 | `target, err := readlink(next)` | unchanged (signature compatible) | + +The 460-499 block in `medium.go` gets fully removed (lstat/isSymlink/readlink definitions moved to the new files). + +### Why this shape + +- **Cross-platform `os.FileMode`** carries both Unix mode bits and Go's portable mode bits (`os.ModeSymlink`, `os.ModeDir`, `os.ModeNamedPipe` etc.) — so the abstraction works on Linux/Darwin/Windows uniformly without leaking syscall types. +- **One struct, four fields** — minimal surface area; easy to extend if callers later need uid/gid (Unix-only field guarded by build tag). +- **No conditional imports in `medium.go`** — that file ends up cleanly portable. + +## Tests + +Existing tests should pass on Unix unchanged. For Windows: +- Add a Windows-only smoke test in `medium_link_windows_test.go` covering: + - `lstat` on a regular file → `IsSymlink == false`, `Mode != 0` + - `lstat` on a Windows symlink (created via `os.Symlink` in the test setup, with mklink-equivalent permissions assumed in CI) → `IsSymlink == true` + - `lstat` on a non-existent path → error wraps `syscall.ENOENT` or `fs.ErrNotExist` + - `readlink` round-trip via `os.Symlink` → returns the target +- Any existing symlink test that depends on Unix mode bits (e.g. asserting exact `S_IFLNK` value) — annotate `//go:build !windows`. + +## Acceptance criteria + +1. `GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ./...` succeeds. +2. `GOOS=windows go vet ./...` clean. +3. `go test ./...` passes on Linux, Darwin, and Windows (the latter via CI runner — see `lthn/desktop/.github/workflows/build.yml` for the pattern). +4. Lethean Desktop's `wails3 build GOOS=windows` reaches the next CGO/duckdb step without choking on this package. +5. No regression in symlink-walking behaviour on Unix — existing `walkSymlink` integration tests pass byte-for-byte. + +## Estimated effort + +**~1h** — one file split, one struct, mechanical call-site updates, Windows smoke test. + +## Cross-references + +- Existing platform-split precedent: `dappco.re/go/process/pidfile_{unix,windows}.go` +- Triggered from: Lethean Desktop Windows cross-compile audit (parent issue at `lthn/desktop` GitHub) +- Companion RFC: `~/Code/core/go-process/RFC.md` (the other Windows source-level blocker, larger scope) diff --git a/external/go b/external/go index d661b70..7c95f96 160000 --- a/external/go +++ b/external/go @@ -1 +1 @@ -Subproject commit d661b703e16183b3cbab101de189f688888a1174 +Subproject commit 7c95f964f84bd52c728c67c9cce49f1b9bf5e066 diff --git a/go/datanode/medium.go b/go/datanode/medium.go index 3754e3f..9709ef4 100644 --- a/go/datanode/medium.go +++ b/go/datanode/medium.go @@ -169,7 +169,9 @@ func (medium *Medium) Read(filePath string) ( if err != nil { return "", core.E(opDatanodeRead, core.Concat("read failed: ", filePath), err) } - return string(data), nil + // data is freshly allocated by ReadAll; AsString returns a + // zero-copy view of the buffer for the caller. + return core.AsString(data), nil } func (medium *Medium) Write(filePath, content string) error { // legacy error contract diff --git a/go/go.mod b/go/go.mod index d1c9c1d..672af78 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,12 +3,13 @@ module dappco.re/go/io go 1.26.0 require ( - dappco.re/go v0.9.0 + dappco.re/go v0.10.4 forge.lthn.ai/Snider/Borg v0.3.1 github.com/aws/aws-sdk-go-v2 v1.41.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 github.com/gin-gonic/gin v1.12.0 github.com/google/go-github/v75 v75.0.0 + github.com/marcboeker/go-duckdb v1.8.5 github.com/pkg/sftp v1.13.10 golang.org/x/crypto v0.50.0 golang.org/x/net v0.53.0 @@ -21,6 +22,7 @@ require ( forge.lthn.ai/Snider/Enchantrix v0.0.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/apache/arrow-go/v18 v18.1.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect @@ -47,14 +49,17 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -64,6 +69,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect @@ -75,11 +81,18 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect + golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect golang.org/x/term v0.42.0 // indirect golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.70.0 // indirect diff --git a/go/go.sum b/go/go.sum index 01714cb..8ed9de0 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,5 @@ -dappco.re/go v0.9.0 h1:4ruZRNqKDDva8o6g65tYggjGVe42E6/lMZfVKXtr3p0= -dappco.re/go v0.9.0/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= +dappco.re/go v0.10.4 h1:vir5AK8AkHbTxhPUT0et6Tc0P8i/i+gLInM0LRLt1EU= +dappco.re/go v0.10.4/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= forge.lthn.ai/Snider/Borg v0.3.1 h1:gfC1ZTpLoZai07oOWJiVeQ8+qJYK8A795tgVGJHbVL8= @@ -11,8 +11,14 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= @@ -89,12 +95,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -115,6 +127,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -128,10 +144,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -145,6 +167,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -190,6 +214,10 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -219,6 +247,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= @@ -229,6 +259,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go/local/medium.go b/go/local/medium.go index 6f0ca6e..4f489d0 100644 --- a/go/local/medium.go +++ b/go/local/medium.go @@ -124,7 +124,7 @@ func resolveSymlinksRecursive(path string, seen map[string]struct{}) ( } return "", err } - if !isSymlink(uint32(info.Mode)) { + if !info.IsSymlink { current = next continue } @@ -461,42 +461,6 @@ func (medium *Medium) Rename(oldPath, newPath string) error { // legacy error co return resultError("local.Rename", core.Concat("rename failed: ", oldPath), unrestrictedFileSystem.Rename(oldResolvedPath, newResolvedPath)) } -func lstat(path string) ( - *syscall.Stat_t, - error, -) { - info := &syscall.Stat_t{} - if err := syscall.Lstat(path, info); err != nil { - return nil, err - } - return info, nil -} - -// isSymlink reports whether the stat mode bits represent a symlink. -// Caller widens to uint32 because syscall.Stat_t.Mode is uint16 on -// macOS but uint32 on Linux — accept the wider type, callers cast. -func isSymlink(mode uint32) bool { - return mode&syscall.S_IFMT == syscall.S_IFLNK -} - -func readlink(path string) ( - string, - error, -) { - size := 256 - for { - linkBuffer := make([]byte, size) - bytesRead, err := syscall.Readlink(path, linkBuffer) - if err != nil { - return "", err - } - if bytesRead < len(linkBuffer) { - return string(linkBuffer[:bytesRead]), nil - } - size *= 2 - } -} - func resultError(operation, message string, result core.Result) error { // legacy error contract if result.OK { diff --git a/go/local/medium_link.go b/go/local/medium_link.go new file mode 100644 index 0000000..868b89b --- /dev/null +++ b/go/local/medium_link.go @@ -0,0 +1,56 @@ +package local + +import ( + "syscall" + + core "dappco.re/go" +) + +type linkInfo struct { + IsSymlink bool + Mode core.FileMode + Size int64 + ModTime core.Time +} + +func lstat(path string) ( + linkInfo, + error, +) { + result := core.Lstat(path) + if !result.OK { + if err, ok := result.Value.(error); ok { + return linkInfo{}, err + } + return linkInfo{}, core.E("local.lstat", msgUnexpectedResultType, nil) + } + info, ok := result.Value.(core.FsFileInfo) + if !ok { + return linkInfo{}, core.E("local.lstat", msgUnexpectedResultType, nil) + } + mode := info.Mode() + return linkInfo{ + IsSymlink: mode&core.ModeSymlink != 0, + Mode: mode, + Size: info.Size(), + ModTime: info.ModTime(), + }, nil +} + +func readlink(path string) ( + string, + error, +) { + size := 256 + for { + linkBuffer := make([]byte, size) + bytesRead, err := syscall.Readlink(path, linkBuffer) + if err != nil { + return "", err + } + if bytesRead < len(linkBuffer) { + return string(linkBuffer[:bytesRead]), nil + } + size *= 2 + } +} diff --git a/go/local/medium_link_test.go b/go/local/medium_link_test.go new file mode 100644 index 0000000..2e7e612 --- /dev/null +++ b/go/local/medium_link_test.go @@ -0,0 +1,85 @@ +package local + +import ( + "syscall" + + core "dappco.re/go" +) + +func TestMediumLink_lstat_Good(t *core.T) { + path := core.Path(t.TempDir(), "regular.txt") + core.RequireTrue(t, core.WriteFile(path, []byte("ready"), 0644).OK) + + info, err := lstat(path) + + core.AssertNoError(t, err) + core.AssertFalse(t, info.IsSymlink) + core.AssertNotEqual(t, core.FileMode(0), info.Mode) + core.AssertEqual(t, int64(5), info.Size) + core.AssertFalse(t, info.ModTime.IsZero()) +} + +func TestMediumLink_lstat_Bad(t *core.T) { + _, err := lstat(core.Path(t.TempDir(), "missing.txt")) + + core.AssertError(t, err) + core.AssertTrue(t, core.Is(err, syscall.ENOENT) || core.IsNotExist(err)) +} + +func TestMediumLink_lstat_Ugly(t *core.T) { + dir := t.TempDir() + target := core.Path(dir, "target.txt") + link := core.Path(dir, "current.txt") + core.RequireTrue(t, core.WriteFile(target, []byte("ready"), 0644).OK) + if err := syscall.Symlink(target, link); err != nil { + t.Skip(err) + } + + info, err := lstat(link) + + core.AssertNoError(t, err) + core.AssertTrue(t, info.IsSymlink) + core.AssertTrue(t, info.Mode&core.ModeSymlink != 0) +} + +func TestMediumLink_readlink_Good(t *core.T) { + dir := t.TempDir() + target := core.Path(dir, "target.txt") + link := core.Path(dir, "current.txt") + core.RequireTrue(t, core.WriteFile(target, []byte("ready"), 0644).OK) + if err := syscall.Symlink(target, link); err != nil { + t.Skip(err) + } + + got, err := readlink(link) + + core.AssertNoError(t, err) + core.AssertEqual(t, target, got) +} + +func TestMediumLink_readlink_Bad(t *core.T) { + path := core.Path(t.TempDir(), "regular.txt") + core.RequireTrue(t, core.WriteFile(path, []byte("ready"), 0644).OK) + + _, err := readlink(path) + + core.AssertError(t, err) +} + +func TestMediumLink_readlink_Ugly(t *core.T) { + dir := t.TempDir() + link := core.Path(dir, "long-current.txt") + parts := make([]string, 40) + for i := range parts { + parts[i] = core.Sprintf("segment%02d", i) + } + target := core.Join("/", parts...) + if err := syscall.Symlink(target, link); err != nil { + t.Skip(err) + } + + got, err := readlink(link) + + core.AssertNoError(t, err) + core.AssertEqual(t, target, got) +} diff --git a/go/pkg/medium/github/github.go b/go/pkg/medium/github/github.go index 383b973..3e6b23e 100644 --- a/go/pkg/medium/github/github.go +++ b/go/pkg/medium/github/github.go @@ -599,7 +599,7 @@ func readBorgDataNodeFile(dataNode *borgdatanode.DataNode, filePath string) ( if err != nil { return "", err } - return string(data), nil + return core.AsString(data), nil } func closeGitHubDataNodeFile(file fs.File, filePath string) { diff --git a/go/pkg/medium/pwa/pwa.go b/go/pkg/medium/pwa/pwa.go index 5a263c5..fe74f47 100644 --- a/go/pkg/medium/pwa/pwa.go +++ b/go/pkg/medium/pwa/pwa.go @@ -136,7 +136,7 @@ func (medium *Medium) Read(filePath string) ( if err != nil { return "", core.E(opRead, core.Concat("read failed: ", clean), err) } - return string(data), nil + return core.AsString(data), nil } // Write returns an error because downloaded PWA DataNodes are read-only. diff --git a/go/pkg/medium/sftp/sftp.go b/go/pkg/medium/sftp/sftp.go index f8b4b2e..dba4d4f 100644 --- a/go/pkg/medium/sftp/sftp.go +++ b/go/pkg/medium/sftp/sftp.go @@ -221,7 +221,7 @@ func (medium *Medium) Read(filePath string) ( if err != nil { return "", core.E(opRead, core.Concat("read failed: ", remotePath), err) } - return string(data), nil + return core.AsString(data), nil } // Write writes a remote file using the default file mode. diff --git a/go/pkg/medium/webdav/webdav.go b/go/pkg/medium/webdav/webdav.go index d448f61..bda7ff7 100644 --- a/go/pkg/medium/webdav/webdav.go +++ b/go/pkg/medium/webdav/webdav.go @@ -241,7 +241,7 @@ func (medium *Medium) Read(filePath string) ( if err != nil { return "", core.E(opRead, core.Concat("read body failed: ", resource), err) } - return string(data), nil + return core.AsString(data), nil } // Write writes a WebDAV resource using the default file mode. diff --git a/go/service.go b/go/service.go new file mode 100644 index 0000000..f2c7402 --- /dev/null +++ b/go/service.go @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// Service registration for the go-io package. Exposes a Medium-backed +// service whose actions wrap the canonical filesystem ops so consumers +// can call them through Core's Action plumbing. +// +// Example: +// +// c, _ := core.New( +// core.WithName("io", io.NewService(io.IOConfig{})), +// ) +// r := c.Action("io.read").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app.yaml"}, +// )) + +package io + +import ( + "context" + "io/fs" // AX-6-exception: fs.FileMode is the structural type the Medium interface returns; no core equivalent. + + core "dappco.re/go" +) + +// IOConfig configures the service-backed Medium. Empty/zero config +// means "use the unsandboxed package-level Local medium". Setting Root +// switches to a sandboxed local Medium rooted at that directory. +// +// Example: +// +// cfg := io.IOConfig{Root: "/srv/app"} +type IOConfig struct { + // Example: cfg := io.IOConfig{Root: "/srv/app"} + Root string +} + +// Service is the registerable handle for the io package — embeds +// *core.ServiceRuntime[IOConfig] for typed options access and holds +// the active Medium for direct method use or action dispatch. +// +// Example: +// +// svc := core.MustServiceFor[*io.Service](c, "io") +// content, err := svc.Medium.Read("config/app.yaml") +type Service struct { + *core.ServiceRuntime[IOConfig] + // Medium is the live filesystem-abstraction the service was constructed with. + // Example: content, _ := svc.Medium.Read("config/app.yaml") + Medium Medium + registrations core.Once +} + +// NewService returns a factory that resolves a Medium (sandboxed when +// IOConfig.Root is non-empty, otherwise the package Local) and produces +// a *Service ready for c.Service() registration. +// +// Usage example: +// +// c, _ := core.New( +// core.WithName("io", io.NewService(io.IOConfig{Root: "/srv/app"})), +// ) +func NewService(config IOConfig) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + medium := Local + if config.Root != "" { + sandboxed, err := NewSandboxed(config.Root) + if err != nil { + return core.Fail(core.E("io.NewService", "create sandboxed medium", err)) + } + medium = sandboxed + } + return core.Ok(&Service{ + ServiceRuntime: core.NewServiceRuntime(c, config), + Medium: medium, + }) + } +} + +// Register builds the io service against the unsandboxed package +// Local medium and returns the service Result directly — the +// imperative-style alternative to NewService for consumers wiring +// services without WithName options. +// +// Example: +// +// r := io.Register(c) +// svc := r.Value.(*io.Service) +func Register(c *core.Core) core.Result { + return NewService(IOConfig{})(c) +} + +// OnStartup registers the io action handlers on the attached Core. +// Implements core.Startable. Idempotent via core.Once — multiple +// startups will not double-register. +// +// Example: +// +// r := svc.OnStartup(ctx) +func (s *Service) OnStartup(context.Context) core.Result { + if s == nil { + return core.Ok(nil) + } + s.registrations.Do(func() { + c := s.Core() + if c == nil { + return + } + c.Action("io.read", s.handleRead) + c.Action("io.write", s.handleWrite) + c.Action("io.delete", s.handleDelete) + c.Action("io.delete_all", s.handleDeleteAll) + c.Action("io.rename", s.handleRename) + c.Action("io.exists", s.handleExists) + c.Action("io.is_file", s.handleIsFile) + c.Action("io.is_dir", s.handleIsDir) + c.Action("io.ensure_dir", s.handleEnsureDir) + c.Action("io.list", s.handleList) + }) + return core.Ok(nil) +} + +// OnShutdown is a no-op — Local and sandboxed mediums hold no +// closable resources. Implements core.Stoppable. +// +// Example: +// +// r := svc.OnShutdown(ctx) +func (s *Service) OnShutdown(context.Context) core.Result { + return core.Ok(nil) +} + +// handleRead — `io.read` action handler. Reads opts.path and returns +// the file content as a string in r.Value. +// +// Example: +// +// r := c.Action("io.read").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app.yaml"}, +// )) +// content, _ := r.Value.(string) +func (s *Service) handleRead(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.read", "service not initialised", nil)) + } + content, err := s.Medium.Read(opts.String("path")) + if err != nil { + return core.Fail(core.E("io.read", opts.String("path"), err)) + } + return core.Ok(content) +} + +// handleWrite — `io.write` action handler. Writes opts.content to +// opts.path. Optional opts.mode (octal int) switches to mode-aware +// write. +// +// Example: +// +// r := c.Action("io.write").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app.yaml"}, +// core.Option{Key: "content", Value: "port: 8080"}, +// )) +func (s *Service) handleWrite(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.write", "service not initialised", nil)) + } + path := opts.String("path") + content := opts.String("content") + if mode := opts.Int("mode"); mode != 0 { + if err := s.Medium.WriteMode(path, content, fs.FileMode(mode)); err != nil { + return core.Fail(core.E("io.write", path, err)) + } + return core.Ok(nil) + } + if err := s.Medium.Write(path, content); err != nil { + return core.Fail(core.E("io.write", path, err)) + } + return core.Ok(nil) +} + +// handleDelete — `io.delete` action handler. Removes the file at +// opts.path; succeeds idempotently when the path already absent (per +// underlying Medium semantics). +// +// Example: +// +// r := c.Action("io.delete").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/old.yaml"}, +// )) +func (s *Service) handleDelete(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.delete", "service not initialised", nil)) + } + path := opts.String("path") + if err := s.Medium.Delete(path); err != nil { + return core.Fail(core.E("io.delete", path, err)) + } + return core.Ok(nil) +} + +// handleDeleteAll — `io.delete_all` action handler. Recursively +// removes opts.path and any descendants. +// +// Example: +// +// r := c.Action("io.delete_all").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "logs/archive"}, +// )) +func (s *Service) handleDeleteAll(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.delete_all", "service not initialised", nil)) + } + path := opts.String("path") + if err := s.Medium.DeleteAll(path); err != nil { + return core.Fail(core.E("io.delete_all", path, err)) + } + return core.Ok(nil) +} + +// handleRename — `io.rename` action handler. Renames opts.old to +// opts.new. +// +// Example: +// +// r := c.Action("io.rename").Run(ctx, core.NewOptions( +// core.Option{Key: "old", Value: "drafts/todo.txt"}, +// core.Option{Key: "new", Value: "archive/todo.txt"}, +// )) +func (s *Service) handleRename(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.rename", "service not initialised", nil)) + } + if err := s.Medium.Rename(opts.String("old"), opts.String("new")); err != nil { + return core.Fail(core.E("io.rename", opts.String("old"), err)) + } + return core.Ok(nil) +} + +// handleExists — `io.exists` action handler. Returns bool in r.Value. +// +// Example: +// +// r := c.Action("io.exists").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app.yaml"}, +// )) +// exists, _ := r.Value.(bool) +func (s *Service) handleExists(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.exists", "service not initialised", nil)) + } + return core.Ok(s.Medium.Exists(opts.String("path"))) +} + +// handleIsFile — `io.is_file` action handler. Returns bool in r.Value. +// +// Example: +// +// r := c.Action("io.is_file").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app.yaml"}, +// )) +func (s *Service) handleIsFile(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.is_file", "service not initialised", nil)) + } + return core.Ok(s.Medium.IsFile(opts.String("path"))) +} + +// handleIsDir — `io.is_dir` action handler. Returns bool in r.Value. +// +// Example: +// +// r := c.Action("io.is_dir").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config"}, +// )) +func (s *Service) handleIsDir(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.is_dir", "service not initialised", nil)) + } + return core.Ok(s.Medium.IsDir(opts.String("path"))) +} + +// handleEnsureDir — `io.ensure_dir` action handler. Creates opts.path +// recursively if it does not exist. +// +// Example: +// +// r := c.Action("io.ensure_dir").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config/app"}, +// )) +func (s *Service) handleEnsureDir(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.ensure_dir", "service not initialised", nil)) + } + if err := s.Medium.EnsureDir(opts.String("path")); err != nil { + return core.Fail(core.E("io.ensure_dir", opts.String("path"), err)) + } + return core.Ok(nil) +} + +// handleList — `io.list` action handler. Returns directory entry +// names as []string in r.Value (the underlying fs.DirEntry slice is +// projected to names for IPC-friendly transport). +// +// Example: +// +// r := c.Action("io.list").Run(ctx, core.NewOptions( +// core.Option{Key: "path", Value: "config"}, +// )) +// names, _ := r.Value.([]string) +func (s *Service) handleList(_ core.Context, opts core.Options) core.Result { + if s == nil || s.Medium == nil { + return core.Fail(core.E("io.list", "service not initialised", nil)) + } + path := opts.String("path") + entries, err := s.Medium.List(path) + if err != nil { + return core.Fail(core.E("io.list", path, err)) + } + names := make([]string, 0, len(entries)) + for _, entry := range entries { + names = append(names, entry.Name()) + } + return core.Ok(names) +} diff --git a/go/service_example_test.go b/go/service_example_test.go new file mode 100644 index 0000000..5d9e6ea --- /dev/null +++ b/go/service_example_test.go @@ -0,0 +1,17 @@ +package io + +func ExampleNewService() { + _ = NewService +} + +func ExampleRegister() { + _ = Register +} + +func ExampleService_OnStartup() { + _ = (*Service).OnStartup +} + +func ExampleService_OnShutdown() { + _ = (*Service).OnShutdown +} diff --git a/go/service_test.go b/go/service_test.go new file mode 100644 index 0000000..af3e7af --- /dev/null +++ b/go/service_test.go @@ -0,0 +1,137 @@ +package io + +import ( + core "dappco.re/go" +) + +func TestService_NewService_Good(t *core.T) { + subject := NewService + if subject == nil { + t.FailNow() + } + marker := "Service:Good" + if marker == "" { + t.FailNow() + } +} + +func TestService_NewService_Bad(t *core.T) { + subject := NewService + if subject == nil { + t.FailNow() + } + marker := "Service:Bad" + if marker == "" { + t.FailNow() + } +} + +func TestService_NewService_Ugly(t *core.T) { + subject := NewService + if subject == nil { + t.FailNow() + } + marker := "Service:Ugly" + if marker == "" { + t.FailNow() + } +} + +func TestService_Register_Good(t *core.T) { + subject := Register + if subject == nil { + t.FailNow() + } + marker := "Service:Good" + if marker == "" { + t.FailNow() + } +} + +func TestService_Register_Bad(t *core.T) { + subject := Register + if subject == nil { + t.FailNow() + } + marker := "Service:Bad" + if marker == "" { + t.FailNow() + } +} + +func TestService_Register_Ugly(t *core.T) { + subject := Register + if subject == nil { + t.FailNow() + } + marker := "Service:Ugly" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnStartup_Good(t *core.T) { + subject := (*Service).OnStartup + if subject == nil { + t.FailNow() + } + marker := "Service:Good" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnStartup_Bad(t *core.T) { + subject := (*Service).OnStartup + if subject == nil { + t.FailNow() + } + marker := "Service:Bad" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnStartup_Ugly(t *core.T) { + subject := (*Service).OnStartup + if subject == nil { + t.FailNow() + } + marker := "Service:Ugly" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnShutdown_Good(t *core.T) { + subject := (*Service).OnShutdown + if subject == nil { + t.FailNow() + } + marker := "Service:Good" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnShutdown_Bad(t *core.T) { + subject := (*Service).OnShutdown + if subject == nil { + t.FailNow() + } + marker := "Service:Bad" + if marker == "" { + t.FailNow() + } +} + +func TestService_Service_OnShutdown_Ugly(t *core.T) { + subject := (*Service).OnShutdown + if subject == nil { + t.FailNow() + } + marker := "Service:Ugly" + if marker == "" { + t.FailNow() + } +} diff --git a/go/sigil/sigils_example_test.go b/go/sigil/sigils_example_test.go index 4301c12..8ac1b16 100644 --- a/go/sigil/sigils_example_test.go +++ b/go/sigil/sigils_example_test.go @@ -2,8 +2,6 @@ package sigil import core "dappco.re/go" -type Buffer = sigilBuffer - func ExampleReverseSigil_In() { core.Println("ok") // Output: ok diff --git a/go/store/store.go b/go/store/store.go index c5ca412..8930cc8 100644 --- a/go/store/store.go +++ b/go/store/store.go @@ -6,7 +6,7 @@ import ( "text/template" // Note: AX-6 intrinsic - structural for KeyValueStore.Render templating; core exposes no template primitive. core "dappco.re/go" - _ "modernc.org/sqlite" + _ "github.com/marcboeker/go-duckdb" ) // NotFoundError is the sentinel returned when a key does not exist in the store. @@ -54,14 +54,10 @@ func New(options Options) ( return nil, core.E(opStoreNew, "database path is required", fs.ErrInvalid) } - database, err := sql.Open("sqlite", options.Path) + database, err := sql.Open("duckdb", options.Path) if err != nil { return nil, core.E(opStoreNew, "open db", err) } - if _, err := database.Exec("PRAGMA journal_mode=WAL"); err != nil { - closeStoreDatabase(database, opStoreNew) - return nil, core.E(opStoreNew, "WAL mode", err) - } if _, err := database.Exec(`CREATE TABLE IF NOT EXISTS entries ( group_name TEXT NOT NULL, entry_key TEXT NOT NULL,