Skip to content

Commit 367cb2d

Browse files
committed
Hosty implemented by GPT-5.3-Codex
1 parent 77e0369 commit 367cb2d

File tree

6 files changed

+473
-10
lines changed

6 files changed

+473
-10
lines changed

.goreleaser.yaml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,17 +227,14 @@ homebrew_casks:
227227
# manpages:
228228
# completions:
229229
directory: Casks
230-
skip_upload: true
231-
commit_author:
232-
name: Andrew Marcuse
233-
email: fileformat@gmail.com
230+
skip_upload: false
234231
description: "Utilities for analyzing, viewing and fixing file formats."
235232
homepage: "https://tools.fileformat.info/"
236233
license: "GPL-3.0-or-later"
237234
repository:
238235
owner: FileFormatInfo
239-
name: fftools-tap
240-
# token:
236+
name: homebrew-tap
237+
token: ${{ secrets.HOMEBREW_TOKEN }}
241238

242239
nfpms:
243240
- id: fftools

cmd/hosty/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Manipulate host/domain names on the command line
44

5+
## Actions
6+
57
* convert to/from punycode
68
* get the public suffix
79
* get the public suffix + 1
@@ -13,6 +15,8 @@ Manipulate host/domain names on the command line
1315
* make sure the TLD is valid
1416
* make sure the public suffix is valid
1517

18+
## Notes
19+
1620
* Handle trailing dot
1721
* Max label size is 63
1822
* Max FQDN is 253

cmd/hosty/hosty.go

Lines changed: 282 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,285 @@
1-
package hosty
1+
package main
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"net"
7+
"os"
8+
"regexp"
9+
"strings"
10+
11+
"github.com/spf13/pflag"
12+
"golang.org/x/net/idna"
13+
"golang.org/x/net/publicsuffix"
14+
)
15+
16+
var (
17+
BUILDER = "unknown"
18+
COMMIT = "(local)"
19+
LASTMOD = "(local)"
20+
VERSION = "internal"
21+
)
22+
23+
//go:embed README.md
24+
var helpText string
25+
26+
var labelRE = regexp.MustCompile(`^[a-z0-9-]+$`)
27+
28+
func stripTrailingDot(host string) string {
29+
if host == "." {
30+
return ""
31+
}
32+
return strings.TrimSuffix(host, ".")
33+
}
34+
35+
func ensureFQDN(host string) string {
36+
h := stripTrailingDot(host)
37+
if h == "" {
38+
return "."
39+
}
40+
return h + "."
41+
}
42+
43+
func toASCIIHost(host string) (string, error) {
44+
if host == "" {
45+
return "", fmt.Errorf("hostname is empty")
46+
}
47+
return idna.Lookup.ToASCII(host)
48+
}
49+
50+
func toUnicodeHost(host string) (string, error) {
51+
if host == "" {
52+
return "", fmt.Errorf("hostname is empty")
53+
}
54+
return idna.Lookup.ToUnicode(host)
55+
}
56+
57+
func tldFromHost(host string) string {
58+
h := stripTrailingDot(host)
59+
if h == "" {
60+
return ""
61+
}
62+
parts := strings.Split(h, ".")
63+
return parts[len(parts)-1]
64+
}
65+
66+
func validHostname(host string) error {
67+
h := stripTrailingDot(host)
68+
if h == "" {
69+
return fmt.Errorf("hostname is empty")
70+
}
71+
72+
ascii, err := toASCIIHost(h)
73+
if err != nil {
74+
return fmt.Errorf("invalid hostname: %w", err)
75+
}
76+
77+
ascii = strings.ToLower(ascii)
78+
if len(ascii) > 253 {
79+
return fmt.Errorf("hostname exceeds 253 octets")
80+
}
81+
82+
labels := strings.Split(ascii, ".")
83+
for _, label := range labels {
84+
if label == "" {
85+
return fmt.Errorf("hostname has an empty label")
86+
}
87+
if len(label) > 63 {
88+
return fmt.Errorf("label %q exceeds 63 octets", label)
89+
}
90+
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
91+
return fmt.Errorf("label %q starts or ends with '-'", label)
92+
}
93+
if !labelRE.MatchString(label) {
94+
return fmt.Errorf("label %q contains invalid characters", label)
95+
}
96+
}
97+
98+
return nil
99+
}
100+
101+
func validTLD(host string) bool {
102+
tld := strings.ToLower(tldFromHost(host))
103+
if tld == "" {
104+
return false
105+
}
106+
suffix, icann := publicsuffix.PublicSuffix(tld)
107+
return icann && suffix == tld
108+
}
109+
110+
func validPublicSuffix(host string) bool {
111+
h := strings.ToLower(stripTrailingDot(host))
112+
if h == "" {
113+
return false
114+
}
115+
suffix, icann := publicsuffix.PublicSuffix(h)
116+
return icann && suffix == h
117+
}
118+
119+
func printValidationResult(ok bool, onInvalid string, original string) {
120+
if ok {
121+
os.Exit(0)
122+
}
123+
124+
switch onInvalid {
125+
case "blank":
126+
fmt.Print("")
127+
os.Exit(0)
128+
case "original":
129+
fmt.Print(original)
130+
os.Exit(0)
131+
default:
132+
os.Exit(1)
133+
}
134+
}
2135

3136
func main() {
137+
var help = pflag.BoolP("help", "h", false, "Show help message")
138+
var version = pflag.Bool("version", false, "Print version information")
139+
140+
var toPunycode = pflag.Bool("to-punycode", false, "Convert hostname to punycode")
141+
var fromPunycode = pflag.Bool("from-punycode", false, "Convert punycode hostname to Unicode")
142+
var getPublicSuffix = pflag.Bool("public-suffix", false, "Output public suffix")
143+
var getETLD1 = pflag.Bool("etld1", false, "Output effective TLD+1 (public suffix plus one label)")
144+
var getTLD = pflag.Bool("tld", false, "Output top-level domain label")
145+
var getBare = pflag.Bool("bare", false, "Output hostname without trailing dot")
146+
var getFQDN = pflag.Bool("fqdn", false, "Output fully qualified hostname with trailing dot")
147+
148+
var checkHostname = pflag.Bool("check-host", false, "Validate hostname syntax and lengths")
149+
var checkResolve = pflag.Bool("check-resolve", false, "Validate hostname by DNS lookup")
150+
var checkTLD = pflag.Bool("check-tld", false, "Validate the TLD against ICANN public suffix data")
151+
var checkSuffix = pflag.Bool("check-suffix", false, "Validate that input is a known public suffix")
152+
var onInvalid = pflag.String("on-invalid", "exit", "Validation failure behavior: exit, blank, original")
153+
154+
pflag.Parse()
155+
156+
if *version {
157+
fmt.Fprintf(os.Stdout, "hosty version %s (built by %s on %s, commit %s)\n", VERSION, BUILDER, LASTMOD, COMMIT)
158+
return
159+
}
160+
161+
if *help {
162+
fmt.Printf("Usage: hosty [options] <hostname>\n\n")
163+
fmt.Printf("%s\n", helpText)
164+
return
165+
}
166+
167+
if *onInvalid != "exit" && *onInvalid != "blank" && *onInvalid != "original" {
168+
fmt.Fprintf(os.Stderr, "ERROR: --on-invalid must be one of: exit, blank, original\n")
169+
os.Exit(1)
170+
}
171+
172+
actions := 0
173+
for _, selected := range []bool{*toPunycode, *fromPunycode, *getPublicSuffix, *getETLD1, *getTLD, *getBare, *getFQDN, *checkHostname, *checkResolve, *checkTLD, *checkSuffix} {
174+
if selected {
175+
actions++
176+
}
177+
}
178+
179+
if actions == 0 {
180+
fmt.Fprintf(os.Stderr, "ERROR: no action selected (try --help)\n")
181+
os.Exit(1)
182+
}
183+
if actions > 1 {
184+
fmt.Fprintf(os.Stderr, "ERROR: select exactly one action\n")
185+
os.Exit(1)
186+
}
187+
188+
args := pflag.Args()
189+
if len(args) == 0 {
190+
fmt.Fprintf(os.Stderr, "ERROR: missing hostname argument\n")
191+
os.Exit(1)
192+
}
193+
if len(args) > 1 {
194+
fmt.Fprintf(os.Stderr, "WARNING: ignoring extra arguments (count=%d)\n", len(args)-1)
195+
}
196+
197+
original := strings.TrimSpace(args[0])
198+
host := original
199+
200+
if *toPunycode {
201+
ascii, err := toASCIIHost(stripTrailingDot(host))
202+
if err != nil {
203+
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
204+
os.Exit(1)
205+
}
206+
if strings.HasSuffix(host, ".") {
207+
fmt.Print(ascii + ".")
208+
return
209+
}
210+
fmt.Print(ascii)
211+
return
212+
}
213+
214+
if *fromPunycode {
215+
unicodeHost, err := toUnicodeHost(stripTrailingDot(host))
216+
if err != nil {
217+
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
218+
os.Exit(1)
219+
}
220+
if strings.HasSuffix(host, ".") {
221+
fmt.Print(unicodeHost + ".")
222+
return
223+
}
224+
fmt.Print(unicodeHost)
225+
return
226+
}
227+
228+
if *getPublicSuffix {
229+
h := strings.ToLower(stripTrailingDot(host))
230+
if h == "" {
231+
fmt.Fprintf(os.Stderr, "ERROR: hostname is empty\n")
232+
os.Exit(1)
233+
}
234+
suffix, _ := publicsuffix.PublicSuffix(h)
235+
fmt.Print(suffix)
236+
return
237+
}
238+
239+
if *getETLD1 {
240+
h := strings.ToLower(stripTrailingDot(host))
241+
etld1, err := publicsuffix.EffectiveTLDPlusOne(h)
242+
if err != nil {
243+
fmt.Fprintf(os.Stderr, "ERROR: unable to derive eTLD+1: %v\n", err)
244+
os.Exit(1)
245+
}
246+
fmt.Print(etld1)
247+
return
248+
}
249+
250+
if *getTLD {
251+
fmt.Print(strings.ToLower(tldFromHost(host)))
252+
return
253+
}
254+
255+
if *getBare {
256+
fmt.Print(stripTrailingDot(host))
257+
return
258+
}
259+
260+
if *getFQDN {
261+
fmt.Print(ensureFQDN(host))
262+
return
263+
}
264+
265+
if *checkHostname {
266+
printValidationResult(validHostname(host) == nil, *onInvalid, original)
267+
}
268+
269+
if *checkResolve {
270+
h := stripTrailingDot(host)
271+
if h == "" {
272+
printValidationResult(false, *onInvalid, original)
273+
}
274+
_, err := net.LookupHost(h)
275+
printValidationResult(err == nil, *onInvalid, original)
276+
}
277+
278+
if *checkTLD {
279+
printValidationResult(validTLD(host), *onInvalid, original)
280+
}
281+
282+
if *checkSuffix {
283+
printValidationResult(validPublicSuffix(host), *onInvalid, original)
284+
}
4285
}

0 commit comments

Comments
 (0)