switch to a safer crontab fork; update deps

This commit is contained in:
Aine 2025-11-04 23:02:19 +00:00
parent 8af0bea527
commit 4b4223b3c5
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
125 changed files with 20059 additions and 142096 deletions

View file

@ -11,11 +11,11 @@ import (
"time"
zlogsentry "github.com/archdx/zerolog-sentry"
"github.com/etkecc/go-crontab"
"github.com/etkecc/go-healthchecks/v2"
"github.com/etkecc/go-linkpearl"
"github.com/getsentry/sentry-go"
_ "github.com/lib/pq"
"github.com/mileusna/crontab"
"github.com/rs/zerolog"
_ "modernc.org/sqlite"
@ -157,6 +157,9 @@ func initSMTP(cfg *config.Config) {
func initCron() {
cron = crontab.New()
cron.SetPanicLogger(func(r any) {
log.Error().Any("recover", r).Msg("cronjob panicked")
})
err := cron.AddJob("* * * * *", q.Process)
if err != nil {

23
go.mod
View file

@ -7,28 +7,28 @@ require (
github.com/emersion/go-msgauth v0.7.0
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/etkecc/go-crontab v1.2.1
github.com/etkecc/go-env v1.2.1
github.com/etkecc/go-fswatcher v1.0.1
github.com/etkecc/go-healthchecks/v2 v2.2.2
github.com/etkecc/go-kit v1.7.5
github.com/etkecc/go-linkpearl v0.0.0-20251009114554-5985fe44a189
github.com/etkecc/go-linkpearl v0.0.0-20251016112544-0d85fb3bccdc
github.com/etkecc/go-mxidwc v1.0.1
github.com/etkecc/go-secgen v1.4.0
github.com/etkecc/go-validator/v2 v2.2.6
github.com/fsnotify/fsnotify v1.9.0
github.com/gabriel-vasile/mimetype v1.4.10
github.com/getsentry/sentry-go v0.35.3
github.com/gabriel-vasile/mimetype v1.4.11
github.com/getsentry/sentry-go v0.36.2
github.com/jhillyerd/enmime/v2 v2.2.0
github.com/kvannotten/mailstrip v0.0.0-20200711213611-0002f5c0467e
github.com/lib/pq v1.10.9
github.com/mcnijman/go-emailaddress v1.1.1
github.com/mileusna/crontab v1.2.0
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.3
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
maunium.net/go/mautrix v0.25.1
modernc.org/sqlite v1.39.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
maunium.net/go/mautrix v0.25.2
modernc.org/sqlite v1.40.0
)
require (
@ -39,7 +39,8 @@ require (
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/etkecc/go-trysmtp v1.1.5 // indirect
github.com/fatih/color v1.18.0 // indirect
@ -61,7 +62,7 @@ require (
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.1 // indirect
github.com/olekukonko/ll v0.1.2 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -73,12 +74,12 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
go.mau.fi/util v0.9.1 // indirect
go.mau.fi/util v0.9.2 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect

50
go.sum
View file

@ -16,8 +16,10 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -31,6 +33,8 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/etkecc/go-crontab v1.2.1 h1:tZM5K0dKt8drUY86+JdxfTQj+6iclgwT7PdeMxXZ4qI=
github.com/etkecc/go-crontab v1.2.1/go.mod h1:L2llhyxQDhbEU3uInVnnSsNZ+1P5NtAUuXkyUN0/BAg=
github.com/etkecc/go-env v1.2.1 h1:b/mIa8D1d9hc3rI8h5bEtBHsnKkBZ6UbmYPog3QIPTU=
github.com/etkecc/go-env v1.2.1/go.mod h1:yTs1DWEsllAZYA417r4V+OmMuYinGXtBzKkLGNvCv5c=
github.com/etkecc/go-fswatcher v1.0.1 h1:n9hqtjzTS3ETb9hcZe1EqYA1lkkhlzZlztu3hXwFDQc=
@ -39,8 +43,8 @@ github.com/etkecc/go-healthchecks/v2 v2.2.2 h1:YV7e+Ga8JY3aZX4Qf6Q1Ca+DnqbT5Drjl
github.com/etkecc/go-healthchecks/v2 v2.2.2/go.mod h1:IowWGN4F6By6z0eh63+639OscylgtdvT9ITjfN6hnZA=
github.com/etkecc/go-kit v1.7.5 h1:DbHTbJ69Jnt2d8mjOyKA9f2ARxvi8xgnYgVMlMZd3gI=
github.com/etkecc/go-kit v1.7.5/go.mod h1:ZFeQrvlMIV6OeZ4XJ090kkyNYRRN4+5wcg1vjnOKEGI=
github.com/etkecc/go-linkpearl v0.0.0-20251009114554-5985fe44a189 h1:isSNC9AsMdUMlnE9tVdTk/DRSNOQx2RvLhKxH3GnpXg=
github.com/etkecc/go-linkpearl v0.0.0-20251009114554-5985fe44a189/go.mod h1:ua01SU6PcE7NtTT5d1wFRUcrqcA5RWbvo9yRLu4LKCA=
github.com/etkecc/go-linkpearl v0.0.0-20251016112544-0d85fb3bccdc h1:jUwnFg8OxXgp6JEmVyuR0sfUqV3pEbnWJqZnrroCM60=
github.com/etkecc/go-linkpearl v0.0.0-20251016112544-0d85fb3bccdc/go.mod h1:bg9M+agdjBrD0nS3uGUh7iP59mwlVZMw2ah+z3JzNoY=
github.com/etkecc/go-mxidwc v1.0.1 h1:t4Kq3FxSlQjt1i7RpzE5q3cOWjJ0vrTzzGZRSpgh8mg=
github.com/etkecc/go-mxidwc v1.0.1/go.mod h1:WFlntcH4mdual/gNi6X7a6rSJERNuLjdrwM3K/tucQA=
github.com/etkecc/go-secgen v1.4.0 h1:PapLpen3aIqG2LDu+U6KWDb4SMW2UGuvetpJjd86yUc=
@ -53,10 +57,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getsentry/sentry-go v0.35.3 h1:u5IJaEqZyPdWqe/hKlBKBBnMTSxB/HenCqF3QLabeds=
github.com/getsentry/sentry-go v0.35.3/go.mod h1:mdL49ixwT2yi57k5eh7mpnDyPybixPzlzEJFu0Z76QA=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getsentry/sentry-go v0.36.2 h1:uhuxRPTrUy0dnSzTd0LrYXlBYygLkKY0hhlG5LXarzM=
github.com/getsentry/sentry-go v0.36.2/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -119,8 +123,6 @@ github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7r
github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/mileusna/crontab v1.2.0 h1:x9ZmE2A4p6CDqMEGQ+GbqsNtnmbdmWMQYShdQu8LvrU=
github.com/mileusna/crontab v1.2.0/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@ -128,8 +130,8 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc=
github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo=
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
@ -173,16 +175,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.1 h1:A+XKHRsjKkFi2qOm4RriR1HqY2hoOXNS3WFHaC89r2Y=
go.mau.fi/util v0.9.1/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.mau.fi/util v0.9.2 h1:+S4Z03iCsGqU2WY8X2gySFsFjaLlUHFRDVCYvVwynKM=
go.mau.fi/util v0.9.2/go.mod h1:055elBBCJSdhRsmub7ci9hXZPgGr1U6dYg44cSgRgoU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
@ -204,8 +206,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -218,8 +220,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.25.1 h1:+xe3eXtQNcDPU/HoWzvSOA5YX57iqlYI1TXf/fM0KWs=
maunium.net/go/mautrix v0.25.1/go.mod h1:iSueLJ/2fBaNrsTObGqi1j0cl/loxrtAjmjay1scYD8=
maunium.net/go/mautrix v0.25.2 h1:CUG23zp754yGOTMh9Q4mVSENS9FyweE/G+6ZsPDMCUU=
maunium.net/go/mautrix v0.25.2/go.mod h1:EWgYyp2iFZP7pnSm+rufHlO8YVnA2KnoNBDpwekiAwI=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
@ -240,8 +242,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

2
vendor/github.com/clipperhouse/stringish/.gitignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
*.test

21
vendor/github.com/clipperhouse/stringish/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Matt Sherman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
vendor/github.com/clipperhouse/stringish/README.md generated vendored Normal file
View file

@ -0,0 +1,64 @@
# stringish
A small Go module that provides a generic type constraint for “string-like”
data, and a utf8 package that works with both strings and byte slices
without conversions.
```go
type Interface interface {
~[]byte | ~string
}
```
[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
## Install
```
go get github.com/clipperhouse/stringish
```
## Examples
```go
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/stringish/utf8"
)
s := "Hello, 世界"
r, size := utf8.DecodeRune(s) // not DecodeRuneInString 🎉
b := []byte("Hello, 世界")
r, size = utf8.DecodeRune(b) // same API!
func MyFoo[T stringish.Interface](s T) T {
// pass a string or a []byte
// iterate, slice, transform, whatever
}
```
## Motivation
Sometimes we want APIs to accept `string` or `[]byte` without having to convert
between those types. That conversion usually allocates!
By implementing with `stringish.Interface`, we can have a single API, and
single implementation for both types: one `Foo` instead of `Foo` and
`FooString`.
We have converted the
[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
as an example -- note the absence of`*InString` funcs. We might look at `x/text`
next.
## Used by
- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
## Prior discussion
- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)

View file

@ -0,0 +1,5 @@
package stringish
type Interface interface {
~[]byte | ~string
}

View file

@ -1,5 +1,9 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## Quick start
```
@ -18,15 +22,14 @@ for tokens.Next() { // Next() returns true until end of data
}
```
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
## Conformance
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29).
![Go](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
## APIs
@ -71,9 +74,18 @@ for tokens.Next() { // Next() returns true until end of data
}
```
### Performance
### Benchmarks
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second, and no allocations.
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/uax29/graphemes/comparative
cpu: Apple M2
BenchmarkGraphemes/clipperhouse/uax29-8 173805 ns/op 201.16 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemes/rivo/uniseg-8 2045128 ns/op 17.10 MB/s 0 B/op 0 allocs/op
```
### Invalid inputs

View file

@ -1,8 +1,11 @@
package graphemes
import "github.com/clipperhouse/uax29/v2/internal/iterators"
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
type Iterator[T iterators.Stringish] struct {
type Iterator[T stringish.Interface] struct {
*iterators.Iterator[T]
}

View file

@ -3,7 +3,7 @@ package graphemes
import (
"bufio"
"github.com/clipperhouse/uax29/v2/internal/iterators"
"github.com/clipperhouse/stringish"
)
// is determines if lookup intersects propert(ies)
@ -18,7 +18,7 @@ const _Ignore = _Extend
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
func splitFunc[T stringish.Interface](data T, atEOF bool) (advance int, token T, err error) {
var empty T
if len(data) == 0 {
return 0, empty, nil

View file

@ -1,10 +1,10 @@
package graphemes
import "github.com/clipperhouse/stringish"
// generated by github.com/clipperhouse/uax29/v2
// from https://www.unicode.org/Public/15.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
import "github.com/clipperhouse/uax29/v2/internal/iterators"
type property uint16
const (
@ -27,7 +27,7 @@ const (
// lookup returns the trie value for the first UTF-8 encoding in s and
// the width in bytes of this encoding. The size will be 0 if s does not
// hold enough bytes to complete the encoding. len(s) must be greater than 0.
func lookup[T iterators.Stringish](s T) (v property, sz int) {
func lookup[T stringish.Interface](s T) (v property, sz int) {
c0 := s[0]
switch {
case c0 < 0x80: // is ASCII

View file

@ -1,14 +1,12 @@
package iterators
type Stringish interface {
[]byte | string
}
import "github.com/clipperhouse/stringish"
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
type SplitFunc[T stringish.Interface] func(T, bool) (int, T, error)
// Iterator is a generic iterator for words that are either []byte or string.
// Iterate while Next() is true, and access the word via Value().
type Iterator[T Stringish] struct {
type Iterator[T stringish.Interface] struct {
split SplitFunc[T]
data T
start int
@ -16,7 +14,7 @@ type Iterator[T Stringish] struct {
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
func New[T stringish.Interface](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
@ -83,3 +81,20 @@ func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
}
advance, _, err := iter.split(iter.data, true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
if advance > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return iter.data[:advance]
}

1
vendor/github.com/etkecc/go-crontab/.gitignore generated vendored Normal file
View file

@ -0,0 +1 @@
/cover.out

150
vendor/github.com/etkecc/go-crontab/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,150 @@
version: "2"
run:
concurrency: 4
modules-download-mode: readonly
issues-exit-code: 1
tests: true
output:
formats:
text:
path: stdout
print-linter-name: true
print-issued-lines: true
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- copyloopvar
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit
- gocritic
- gocyclo
- gosec
- gosmopolitan
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- unconvert
- unparam
- usestdlibvars
- wastedassign
settings:
decorder:
dec-order:
- const
- var
- type
- func
dogsled:
max-blank-identifiers: 3
errcheck:
check-type-assertions: true
check-blank: true
errchkjson:
report-no-exported: true
exhaustive:
check:
- switch
- map
default-signifies-exhaustive: true
gocognit:
min-complexity: 15
gocritic:
enabled-tags:
- diagnostic
- style
- performance
grouper:
const-require-single-const: true
import-require-single-import: true
var-require-single-var: true
misspell:
locale: US
nestif:
min-complexity: 5
unparam:
check-exported: true
usestdlibvars:
time-month: true
time-layout: true
crypto-hash: true
default-rpc-path: true
sql-isolation-level: true
tls-signature-scheme: true
constant-kind: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- dupl
- errcheck
- gocognit
- gocyclo
- gosec
path: _test\.go
- linters:
- staticcheck
text: 'SA9003:'
- linters:
- lll
source: '^//go:generate '
- linters:
- revive
text: returns unexported type
- linters:
- revive
text: 'var-naming: avoid meaningless package names'
paths:
- mocks
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
new: false
formatters:
enable:
- gofmt
- gofumpt
- goimports
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
- pattern: a[b:len(a)]
replacement: a[b:]
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- mocks
- third_party$
- builtin$
- examples$

View file

@ -1,10 +1,16 @@
# Go/Golang package for Crontab tickers [![GoDoc](https://godoc.org/github.com/mileusna/crontab?status.svg)](https://godoc.org/github.com/mileusna/crontab)
# Go/Golang package for Crontab tickers [![GoDoc](https://godoc.org/github.com/etkecc/go-crontab?status.svg)](https://godoc.org/github.com/etkecc/go-crontab)
This package provides crontab tickers to golang apps, supporting crontab-like syntax like `* * * * *` or `*/2 * * * *` etc.
This is a fork of [github.com/mileusna/crontab](https://github.com/mileusna/crontab) with small modifications:
- added `SetPanicLogger` function to set custom panic logger function used in panic recovery
- added linter and refactored code to pass linter checks
- changed minimal go version to 1.22
## Installation <a id="installation"></a>
```
go get github.com/mileusna/crontab
go get github.com/etkecc/go-crontab
```
## Example<a id="example"></a>
@ -16,7 +22,7 @@ import (
"fmt"
"log"
"github.com/mileusna/crontab"
"github.com/etkecc/go-crontab"
)
func main() {

View file

@ -14,9 +14,10 @@ import (
// Crontab struct representing cron table
type Crontab struct {
mu sync.RWMutex
ticker *time.Ticker
jobs []*job
sync.RWMutex
paniclog func(any)
}
// job in cron table
@ -27,8 +28,9 @@ type job struct {
month map[int]struct{}
dayOfWeek map[int]struct{}
fn interface{}
args []interface{}
fn any
args []any
paniclog func(any)
sync.RWMutex
}
@ -43,14 +45,17 @@ type tick struct {
// New initializes and returns new cron table
func New() *Crontab {
return new(time.Minute)
return newCrontab(time.Minute)
}
// new creates new crontab, arg provided for testing purpose
func new(t time.Duration) *Crontab {
// newCrontab creates newCrontab crontab, arg provided for testing purpose
func newCrontab(t time.Duration) *Crontab {
c := &Crontab{
ticker: time.NewTicker(t),
jobs: []*job{},
paniclog: func(r any) {
log.Println("Crontab error", r)
},
}
go func() {
@ -71,21 +76,21 @@ func new(t time.Duration) *Crontab {
// * fn is not function
//
// * Provided args don't match the number and/or the type of fn args
func (c *Crontab) AddJob(schedule string, fn interface{}, args ...interface{}) error {
func (c *Crontab) AddJob(schedule string, fn any, args ...any) error {
j, err := parseSchedule(schedule)
c.Lock()
defer c.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
if err != nil {
return err
}
if fn == nil || reflect.ValueOf(fn).Kind() != reflect.Func {
return fmt.Errorf("Cron job must be func()")
return errors.New("cron job must be func()")
}
fnType := reflect.TypeOf(fn)
if len(args) != fnType.NumIn() {
return fmt.Errorf("Number of func() params and number of provided params doesn't match")
return errors.New("number of func() params and number of provided params doesn't match")
}
for i := 0; i < fnType.NumIn(); i++ {
@ -95,10 +100,10 @@ func (c *Crontab) AddJob(schedule string, fn interface{}, args ...interface{}) e
if t1 != t2 {
if t1.Kind() != reflect.Interface {
return fmt.Errorf("Param with index %d shold be `%s` not `%s`", i, t1, t2)
return fmt.Errorf("param with index %d shold be `%s` not `%s`", i, t1, t2)
}
if !t2.Implements(t1) {
return fmt.Errorf("Param with index %d of type `%s` doesn't implement interface `%s`", i, t2, t1)
return fmt.Errorf("param with index %d of type `%s` doesn't implement interface `%s`", i, t2, t1)
}
}
}
@ -106,13 +111,14 @@ func (c *Crontab) AddJob(schedule string, fn interface{}, args ...interface{}) e
// all checked, add job to cron tab
j.fn = fn
j.args = args
j.paniclog = c.paniclog
c.jobs = append(c.jobs, j)
return nil
}
// MustAddJob is like AddJob but panics if there is an problem with job
//
// It simplifies initialization, since we usually add jobs at the beggining so you won't have to check for errors (it will panic when program starts).
// It simplifies initialization, since we usually add jobs at the beginning so you won't have to check for errors (it will panic when program starts).
// It is a similar aproach as go's std lib package `regexp` and `regexp.Compile()` `regexp.MustCompile()`
// MustAddJob will panic if:
//
@ -121,31 +127,36 @@ func (c *Crontab) AddJob(schedule string, fn interface{}, args ...interface{}) e
// * fn is not function
//
// * Provided args don't match the number and/or the type of fn args
func (c *Crontab) MustAddJob(schedule string, fn interface{}, args ...interface{}) {
func (c *Crontab) MustAddJob(schedule string, fn any, args ...any) {
if err := c.AddJob(schedule, fn, args...); err != nil {
panic(err)
}
}
// SetPanicLogger function to set custom panic logger
func (c *Crontab) SetPanicLogger(f func(any)) {
c.paniclog = f
}
// Shutdown the cron table schedule
//
// Once stopped, it can't be restarted.
// This function is pre-shuttdown helper for your app, there is no Start/Stop functionallity with crontab package.
// This function is pre-shuttdown helper for your app, there is no Start/Stop functionality with crontab package.
func (c *Crontab) Shutdown() {
c.ticker.Stop()
}
// Clear all jobs from cron table
func (c *Crontab) Clear() {
c.Lock()
c.mu.Lock()
c.jobs = []*job{}
c.Unlock()
c.mu.Unlock()
}
// RunAll jobs in cron table, shcheduled or not
func (c *Crontab) RunAll() {
c.RLock()
defer c.RUnlock()
c.mu.RLock()
defer c.mu.RUnlock()
for _, j := range c.jobs {
go j.run()
}
@ -154,8 +165,8 @@ func (c *Crontab) RunAll() {
// RunScheduled jobs
func (c *Crontab) runScheduled(t time.Time) {
tick := getTick(t)
c.RLock()
defer c.RUnlock()
c.mu.RLock()
defer c.mu.RUnlock()
for _, j := range c.jobs {
if j.tick(tick) {
@ -170,7 +181,7 @@ func (j *job) run() {
j.RLock()
defer func() {
if r := recover(); r != nil {
log.Println("Crontab error", r)
j.paniclog(r)
}
}()
v := reflect.ValueOf(j.fn)
@ -224,7 +235,7 @@ func parseSchedule(s string) (*job, error) {
s = matchSpaces.ReplaceAllLiteralString(s, " ")
parts := strings.Split(s, " ")
if len(parts) != 5 {
return j, errors.New("Schedule string must have five components like * * * * *")
return j, errors.New("schedule string must have five components like * * * * *")
}
j.min, err = parsePart(parts[0], 0, 59)
@ -267,34 +278,35 @@ func parseSchedule(s string) (*job, error) {
}
// parsePart parse individual schedule part from schedule string
func parsePart(s string, min, max int) (map[int]struct{}, error) {
//
//nolint:gocognit // TODO: refactor
func parsePart(s string, minimum, maximum int) (map[int]struct{}, error) {
r := make(map[int]struct{})
// wildcard pattern
if s == "*" {
for i := min; i <= max; i++ {
for i := minimum; i <= maximum; i++ {
r[i] = struct{}{}
}
return r, nil
}
// */2 1-59/5 pattern
if matches := matchN.FindStringSubmatch(s); matches != nil {
localMin := min
localMax := max
if matches := matchN.FindStringSubmatch(s); matches != nil { //nolint:nestif // TODO: refactor
localMin := minimum
localMax := maximum
if matches[1] != "" && matches[1] != "*" {
if rng := matchRange.FindStringSubmatch(matches[1]); rng != nil {
localMin, _ = strconv.Atoi(rng[1])
localMax, _ = strconv.Atoi(rng[2])
if localMin < min || localMax > max {
return nil, fmt.Errorf("Out of range for %s in %s. %s must be in range %d-%d", rng[1], s, rng[1], min, max)
localMin, _ = strconv.Atoi(rng[1]) //nolint:errcheck // checked below
localMax, _ = strconv.Atoi(rng[2]) //nolint:errcheck // checked below
if localMin < minimum || localMax > maximum {
return nil, fmt.Errorf("out of range for %s in %s. %s must be in range %d-%d", rng[1], s, rng[1], minimum, maximum)
}
} else {
return nil, fmt.Errorf("Unable to parse %s part in %s", matches[1], s)
return nil, fmt.Errorf("unable to parse %s part in %s", matches[1], s)
}
}
n, _ := strconv.Atoi(matches[2])
n, _ := strconv.Atoi(matches[2]) //nolint:errcheck // regexp ensures this is number
for i := localMin; i <= localMax; i += n {
r[i] = struct{}{}
}
@ -305,26 +317,26 @@ func parsePart(s string, min, max int) (map[int]struct{}, error) {
parts := strings.Split(s, ",")
for _, x := range parts {
if rng := matchRange.FindStringSubmatch(x); rng != nil {
localMin, _ := strconv.Atoi(rng[1])
localMax, _ := strconv.Atoi(rng[2])
if localMin < min || localMax > max {
return nil, fmt.Errorf("Out of range for %s in %s. %s must be in range %d-%d", x, s, x, min, max)
localMin, _ := strconv.Atoi(rng[1]) //nolint:errcheck // checked below
localMax, _ := strconv.Atoi(rng[2]) //nolint:errcheck // checked below
if localMin < minimum || localMax > maximum {
return nil, fmt.Errorf("out of range for %s in %s. %s must be in range %d-%d", x, s, x, minimum, maximum)
}
for i := localMin; i <= localMax; i++ {
r[i] = struct{}{}
}
} else if i, err := strconv.Atoi(x); err == nil {
if i < min || i > max {
return nil, fmt.Errorf("Out of range for %d in %s. %d must be in range %d-%d", i, s, i, min, max)
if i < minimum || i > maximum {
return nil, fmt.Errorf("out of range for %d in %s. %d must be in range %d-%d", i, s, i, minimum, maximum)
}
r[i] = struct{}{}
} else {
return nil, fmt.Errorf("Unable to parse %s part in %s", x, s)
return nil, fmt.Errorf("unable to parse %s part in %s", x, s)
}
}
if len(r) == 0 {
return nil, fmt.Errorf("Unable to parse %s", s)
return nil, fmt.Errorf("unable to parse %s", s)
}
return r, nil
@ -340,3 +352,5 @@ func getTick(t time.Time) tick {
dayOfWeek: int(t.Weekday()),
}
}
// vim: ft=go

22
vendor/github.com/etkecc/go-crontab/justfile generated vendored Normal file
View file

@ -0,0 +1,22 @@
# show help by default
default:
@just --list --justfile {{ justfile() }}
# update go deps
update *flags:
go get {{ flags }} .
go mod tidy
# run linter
lint:
golangci-lint run ./...
# automatically fix liter issues
lintfix:
golangci-lint run --fix ./...
# run unit tests
test:
@go test -cover -coverprofile=cover.out -coverpkg=./... -covermode=set ./...
@go tool cover -func=cover.out
-@rm -f cover.out

View file

@ -0,0 +1,9 @@
# Github is obeying this ignore file by default.
# Run this command on local to ignore formatting commits in `git blame`
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Added a new column to supported_mimes.md
# The supported_mimes.md file was a nice way to find when a file format was
# introduced. However, when I changed to add a new column in the table, the
# whole git blame got poisoned for the file.
eb497f9bc5d31c6eab2929a112051218670137ba

View file

@ -97,7 +97,9 @@ or from a [file](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#DetectFil
</div>
## Contributing
Contributions are unexpected but welcome. When submitting a PR for detection of
a new file format, please make sure to add a record to the list of testcases
from [mimetype_test.go](mimetype_test.go). For complex files a record can be added
in the [testdata](testdata) directory.
Contributions are never expected but very much welcome.
[mimetype_tests](https://github.com/gabriel-vasile/mimetype_tests/actions/workflows/test.yml)
shows which file formats are most often misidentified and can help prioritise.
When submitting a PR for detection of a new file format, please make sure to
add a record to the list of testcases in [mimetype_test.go](mimetype_test.go).
For complex files a record can be added in the [testdata](testdata) directory.

View file

@ -2,6 +2,7 @@ package charset
import (
"bytes"
"strings"
"unicode/utf8"
"github.com/gabriel-vasile/mimetype/internal/markup"
@ -141,27 +142,25 @@ func FromXML(content []byte) string {
return FromPlain(content)
}
func fromXML(s scan.Bytes) string {
xml := []byte("<?XML")
xml := []byte("<?xml")
lxml := len(xml)
for {
if len(s) == 0 {
return ""
}
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
s.TrimLWS()
if len(s) <= lxml {
return ""
}
if !s.Match(xml, scan.IgnoreCase) {
s = s[1:] // safe to slice instead of s.Advance(1) because bounds are checked
continue
i, k := s.Search(xml, 0)
if i == -1 {
return ""
}
aName, aVal, hasMore := "", "", true
s.Advance(i + k)
var aName, aVal []byte
hasMore := true
for hasMore {
aName, aVal, hasMore = markup.GetAnAttribute(&s)
if aName == "encoding" && aVal != "" {
return aVal
if scan.Bytes(aName).Match([]byte("encoding"), 0) != -1 && len(aVal) != 0 {
return string(aVal)
}
}
}
@ -198,10 +197,10 @@ func fromHTML(s scan.Bytes) string {
return ""
}
// Abort when <body is reached.
if s.Match(body, scan.IgnoreCase) {
if s.Match(body, scan.IgnoreCase) != -1 {
return ""
}
if !s.Match(meta, scan.IgnoreCase) {
if s.Match(meta, scan.IgnoreCase) == -1 {
s = s[1:] // safe to slice instead of s.Advance(1) because bounds are checked
continue
}
@ -215,14 +214,16 @@ func fromHTML(s scan.Bytes) string {
needPragma := dontKnow
charset := ""
aName, aVal, hasMore := "", "", true
var aNameB, aValB []byte
hasMore := true
for hasMore {
aName, aVal, hasMore = markup.GetAnAttribute(&s)
aNameB, aValB, hasMore = markup.GetAnAttribute(&s)
aName := strings.ToLower(string(aNameB))
if attrList[aName] {
continue
}
// processing step
if len(aName) == 0 && len(aVal) == 0 {
if len(aName) == 0 && len(aValB) == 0 {
if needPragma == dontKnow {
continue
}
@ -231,15 +232,18 @@ func fromHTML(s scan.Bytes) string {
}
}
attrList[aName] = true
if aName == "http-equiv" && scan.Bytes(aVal).Match([]byte("CONTENT-TYPE"), scan.IgnoreCase) {
switch aName {
case "http-equiv":
if scan.Bytes(aValB).Match([]byte("CONTENT-TYPE"), scan.IgnoreCase) != -1 {
gotPragma = true
} else if aName == "content" {
charset = string(extractCharsetFromMeta(scan.Bytes(aVal)))
}
case "content":
charset = string(extractCharsetFromMeta(scan.Bytes(aValB)))
if len(charset) != 0 {
needPragma = doNeedPragma
}
} else if aName == "charset" {
charset = aVal
case "charset":
charset = string(aValB)
needPragma = doNotNeedPragma
}
}

View file

@ -257,8 +257,11 @@ out:
return 0
}
// openArray is used instead of an inline []byte{'['} to avoid mem alllocs.
var openArray = []byte{'['}
func (p *parserState) consumeArray(b []byte, qs []query, lvl int) (n int) {
p.appendPath([]byte{'['}, qs)
p.appendPath(openArray, qs)
if len(b) == 0 {
return 0
}

View file

@ -5,43 +5,80 @@ import (
"encoding/binary"
)
var (
// SevenZ matches a 7z archive.
SevenZ = prefix([]byte{0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C})
// Gzip matches gzip files based on http://www.zlib.org/rfc-gzip.html#header-trailer.
Gzip = prefix([]byte{0x1f, 0x8b})
// Fits matches an Flexible Image Transport System file.
Fits = prefix([]byte{
// SevenZ matches a 7z archive.
func SevenZ(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C})
}
// Gzip matches gzip files based on http://www.zlib.org/rfc-gzip.html#header-trailer.
func Gzip(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x1f, 0x8b})
}
// Fits matches an Flexible Image Transport System file.
func Fits(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{
0x53, 0x49, 0x4D, 0x50, 0x4C, 0x45, 0x20, 0x20, 0x3D, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x54,
})
// Xar matches an eXtensible ARchive format file.
Xar = prefix([]byte{0x78, 0x61, 0x72, 0x21})
// Bz2 matches a bzip2 file.
Bz2 = prefix([]byte{0x42, 0x5A, 0x68})
// Ar matches an ar (Unix) archive file.
Ar = prefix([]byte{0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E})
// Deb matches a Debian package file.
Deb = offset([]byte{
}
// Xar matches an eXtensible ARchive format file.
func Xar(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x78, 0x61, 0x72, 0x21})
}
// Bz2 matches a bzip2 file.
func Bz2(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x42, 0x5A, 0x68})
}
// Ar matches an ar (Unix) archive file.
func Ar(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E})
}
// Deb matches a Debian package file.
func Deb(raw []byte, _ uint32) bool {
return offset(raw, []byte{
0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x2D,
0x62, 0x69, 0x6E, 0x61, 0x72, 0x79,
}, 8)
// Warc matches a Web ARChive file.
Warc = prefix([]byte("WARC/1.0"), []byte("WARC/1.1"))
// Cab matches a Microsoft Cabinet archive file.
Cab = prefix([]byte("MSCF\x00\x00\x00\x00"))
// Xz matches an xz compressed stream based on https://tukaani.org/xz/xz-file-format.txt.
Xz = prefix([]byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00})
// Lzip matches an Lzip compressed file.
Lzip = prefix([]byte{0x4c, 0x5a, 0x49, 0x50})
// RPM matches an RPM or Delta RPM package file.
RPM = prefix([]byte{0xed, 0xab, 0xee, 0xdb}, []byte("drpm"))
// Cpio matches a cpio archive file.
Cpio = prefix([]byte("070707"), []byte("070701"), []byte("070702"))
// RAR matches a RAR archive file.
RAR = prefix([]byte("Rar!\x1A\x07\x00"), []byte("Rar!\x1A\x07\x01\x00"))
)
}
// Warc matches a Web ARChive file.
func Warc(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("WARC/1.0")) ||
bytes.HasPrefix(raw, []byte("WARC/1.1"))
}
// Cab matches a Microsoft Cabinet archive file.
func Cab(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("MSCF\x00\x00\x00\x00"))
}
// Xz matches an xz compressed stream based on https://tukaani.org/xz/xz-file-format.txt.
func Xz(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00})
}
// Lzip matches an Lzip compressed file.
func Lzip(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x4c, 0x5a, 0x49, 0x50})
}
// RPM matches an RPM or Delta RPM package file.
func RPM(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0xed, 0xab, 0xee, 0xdb}) ||
bytes.HasPrefix(raw, []byte("drpm"))
}
// RAR matches a RAR archive file.
func RAR(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("Rar!\x1A\x07\x00")) ||
bytes.HasPrefix(raw, []byte("Rar!\x1A\x07\x01\x00"))
}
// InstallShieldCab matches an InstallShield Cabinet archive file.
func InstallShieldCab(raw []byte, _ uint32) bool {
@ -78,6 +115,17 @@ func CRX(raw []byte, limit uint32) bool {
return Zip(raw[zipOffset:], limit)
}
// Cpio matches a cpio archive file.
func Cpio(raw []byte, _ uint32) bool {
if len(raw) < 6 {
return false
}
return binary.LittleEndian.Uint16(raw) == 070707 || // binary cpio
bytes.HasPrefix(raw, []byte("070707")) || // portable ASCII cpios
bytes.HasPrefix(raw, []byte("070701")) ||
bytes.HasPrefix(raw, []byte("070702"))
}
// Tar matches a (t)ape (ar)chive file.
// Tar files are divided into 512 bytes records. First record contains a 257
// bytes header padded with NUL.

View file

@ -5,26 +5,50 @@ import (
"encoding/binary"
)
var (
// Flac matches a Free Lossless Audio Codec file.
Flac = prefix([]byte("\x66\x4C\x61\x43\x00\x00\x00\x22"))
// Midi matches a Musical Instrument Digital Interface file.
Midi = prefix([]byte("\x4D\x54\x68\x64"))
// Ape matches a Monkey's Audio file.
Ape = prefix([]byte("\x4D\x41\x43\x20\x96\x0F\x00\x00\x34\x00\x00\x00\x18\x00\x00\x00\x90\xE3"))
// MusePack matches a Musepack file.
MusePack = prefix([]byte("MPCK"))
// Au matches a Sun Microsystems au file.
Au = prefix([]byte("\x2E\x73\x6E\x64"))
// Amr matches an Adaptive Multi-Rate file.
Amr = prefix([]byte("\x23\x21\x41\x4D\x52"))
// Voc matches a Creative Voice file.
Voc = prefix([]byte("Creative Voice File"))
// M3u matches a Playlist file.
M3u = prefix([]byte("#EXTM3U"))
// AAC matches an Advanced Audio Coding file.
AAC = prefix([]byte{0xFF, 0xF1}, []byte{0xFF, 0xF9})
)
// Flac matches a Free Lossless Audio Codec file.
func Flac(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x66\x4C\x61\x43\x00\x00\x00\x22"))
}
// Midi matches a Musical Instrument Digital Interface file.
func Midi(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x4D\x54\x68\x64"))
}
// Ape matches a Monkey's Audio file.
func Ape(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x4D\x41\x43\x20\x96\x0F\x00\x00\x34\x00\x00\x00\x18\x00\x00\x00\x90\xE3"))
}
// MusePack matches a Musepack file.
func MusePack(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("MPCK"))
}
// Au matches a Sun Microsystems au file.
func Au(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x2E\x73\x6E\x64"))
}
// Amr matches an Adaptive Multi-Rate file.
func Amr(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x23\x21\x41\x4D\x52"))
}
// Voc matches a Creative Voice file.
func Voc(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("Creative Voice File"))
}
// M3u matches a Playlist file.
func M3u(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("#EXTM3U"))
}
// AAC matches an Advanced Audio Coding file.
func AAC(raw []byte, _ uint32) bool {
return len(raw) > 1 && ((raw[0] == 0xFF && raw[1] == 0xF1) || (raw[0] == 0xFF && raw[1] == 0xF9))
}
// Mp3 matches an mp3 file.
func Mp3(raw []byte, limit uint32) bool {

View file

@ -6,26 +6,52 @@ import (
"encoding/binary"
)
var (
// Lnk matches Microsoft lnk binary format.
Lnk = prefix([]byte{0x4C, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00})
// Wasm matches a web assembly File Format file.
Wasm = prefix([]byte{0x00, 0x61, 0x73, 0x6D})
// Exe matches a Windows/DOS executable file.
Exe = prefix([]byte{0x4D, 0x5A})
// Elf matches an Executable and Linkable Format file.
Elf = prefix([]byte{0x7F, 0x45, 0x4C, 0x46})
// Nes matches a Nintendo Entertainment system ROM file.
Nes = prefix([]byte{0x4E, 0x45, 0x53, 0x1A})
// SWF matches an Adobe Flash swf file.
SWF = prefix([]byte("CWS"), []byte("FWS"), []byte("ZWS"))
// Torrent has bencoded text in the beginning.
Torrent = prefix([]byte("d8:announce"))
// PAR1 matches a parquet file.
Par1 = prefix([]byte{0x50, 0x41, 0x52, 0x31})
// CBOR matches a Concise Binary Object Representation https://cbor.io/
CBOR = prefix([]byte{0xD9, 0xD9, 0xF7})
)
// Lnk matches Microsoft lnk binary format.
func Lnk(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x4C, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00})
}
// Wasm matches a web assembly File Format file.
func Wasm(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x00, 0x61, 0x73, 0x6D})
}
// Exe matches a Windows/DOS executable file.
func Exe(raw []byte, _ uint32) bool {
return len(raw) > 1 && raw[0] == 0x4D && raw[1] == 0x5A
}
// Elf matches an Executable and Linkable Format file.
func Elf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x7F, 0x45, 0x4C, 0x46})
}
// Nes matches a Nintendo Entertainment system ROM file.
func Nes(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x4E, 0x45, 0x53, 0x1A})
}
// SWF matches an Adobe Flash swf file.
func SWF(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("CWS")) ||
bytes.HasPrefix(raw, []byte("FWS")) ||
bytes.HasPrefix(raw, []byte("ZWS"))
}
// Torrent has bencoded text in the beginning.
func Torrent(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("d8:announce"))
}
// PAR1 matches a parquet file.
func Par1(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x50, 0x41, 0x52, 0x31})
}
// CBOR matches a Concise Binary Object Representation https://cbor.io/
func CBOR(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0xD9, 0xD9, 0xF7})
}
// Java bytecode and Mach-O binaries share the same magic number.
// More info here https://github.com/threatstack/libmagic/blob/master/magic/Magdir/cafebabe
@ -168,8 +194,10 @@ func Marc(raw []byte, limit uint32) bool {
//
// [glTF specification]: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
// [IANA glTF entry]: https://www.iana.org/assignments/media-types/model/gltf-binary
var GLB = prefix([]byte("\x67\x6C\x54\x46\x02\x00\x00\x00"),
[]byte("\x67\x6C\x54\x46\x01\x00\x00\x00"))
func GLB(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x67\x6C\x54\x46\x02\x00\x00\x00")) ||
bytes.HasPrefix(raw, []byte("\x67\x6C\x54\x46\x01\x00\x00\x00"))
}
// TzIf matches a Time Zone Information Format (TZif) file.
// See more: https://tools.ietf.org/id/draft-murchison-tzdist-tzif-00.html#rfc.section.3

View file

@ -1,13 +1,21 @@
package magic
var (
// Sqlite matches an SQLite database file.
Sqlite = prefix([]byte{
import "bytes"
// Sqlite matches an SQLite database file.
func Sqlite(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66,
0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
})
// MsAccessAce matches Microsoft Access dababase file.
MsAccessAce = offset([]byte("Standard ACE DB"), 4)
// MsAccessMdb matches legacy Microsoft Access database file (JET, 2003 and earlier).
MsAccessMdb = offset([]byte("Standard Jet DB"), 4)
)
}
// MsAccessAce matches Microsoft Access dababase file.
func MsAccessAce(raw []byte, _ uint32) bool {
return offset(raw, []byte("Standard ACE DB"), 4)
}
// MsAccessMdb matches legacy Microsoft Access database file (JET, 2003 and earlier).
func MsAccessMdb(raw []byte, _ uint32) bool {
return offset(raw, []byte("Standard Jet DB"), 4)
}

View file

@ -5,14 +5,31 @@ import (
"encoding/binary"
)
var (
// Fdf matches a Forms Data Format file.
Fdf = prefix([]byte("%FDF"))
// Mobi matches a Mobi file.
Mobi = offset([]byte("BOOKMOBI"), 60)
// Lit matches a Microsoft Lit file.
Lit = prefix([]byte("ITOLITLS"))
)
// Pdf matches a Portable Document Format file.
// https://github.com/file/file/blob/11010cc805546a3e35597e67e1129a481aed40e8/magic/Magdir/pdf
func Pdf(raw []byte, _ uint32) bool {
// usual pdf signature
return bytes.HasPrefix(raw, []byte("%PDF-")) ||
// new-line prefixed signature
bytes.HasPrefix(raw, []byte("\012%PDF-")) ||
// UTF-8 BOM prefixed signature
bytes.HasPrefix(raw, []byte("\xef\xbb\xbf%PDF-"))
}
// Fdf matches a Forms Data Format file.
func Fdf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("%FDF"))
}
// Mobi matches a Mobi file.
func Mobi(raw []byte, _ uint32) bool {
return offset(raw, []byte("BOOKMOBI"), 60)
}
// Lit matches a Microsoft Lit file.
func Lit(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("ITOLITLS"))
}
// PDF matches a Portable Document Format file.
// The %PDF- header should be the first thing inside the file but many

View file

@ -4,14 +4,20 @@ import (
"bytes"
)
var (
// Woff matches a Web Open Font Format file.
Woff = prefix([]byte("wOFF"))
// Woff2 matches a Web Open Font Format version 2 file.
Woff2 = prefix([]byte("wOF2"))
// Otf matches an OpenType font file.
Otf = prefix([]byte{0x4F, 0x54, 0x54, 0x4F, 0x00})
)
// Woff matches a Web Open Font Format file.
func Woff(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("wOFF"))
}
// Woff2 matches a Web Open Font Format version 2 file.
func Woff2(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("wOF2"))
}
// Otf matches an OpenType font file.
func Otf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x4F, 0x54, 0x54, 0x4F, 0x00})
}
// Ttf matches a TrueType font file.
func Ttf(raw []byte, limit uint32) bool {

View file

@ -4,24 +4,33 @@ import (
"bytes"
)
var (
// AVIF matches an AV1 Image File Format still or animated.
// Wikipedia page seems outdated listing image/avif-sequence for animations.
// https://github.com/AOMediaCodec/av1-avif/issues/59
AVIF = ftyp([]byte("avif"), []byte("avis"))
// ThreeGP matches a 3GPP file.
ThreeGP = ftyp(
// AVIF matches an AV1 Image File Format still or animated.
// Wikipedia page seems outdated listing image/avif-sequence for animations.
// https://github.com/AOMediaCodec/av1-avif/issues/59
func AVIF(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("avif"), []byte("avis"))
}
// ThreeGP matches a 3GPP file.
func ThreeGP(raw []byte, _ uint32) bool {
return ftyp(raw,
[]byte("3gp1"), []byte("3gp2"), []byte("3gp3"), []byte("3gp4"),
[]byte("3gp5"), []byte("3gp6"), []byte("3gp7"), []byte("3gs7"),
[]byte("3ge6"), []byte("3ge7"), []byte("3gg6"),
)
// ThreeG2 matches a 3GPP2 file.
ThreeG2 = ftyp(
}
// ThreeG2 matches a 3GPP2 file.
func ThreeG2(raw []byte, _ uint32) bool {
return ftyp(raw,
[]byte("3g24"), []byte("3g25"), []byte("3g26"), []byte("3g2a"),
[]byte("3g2b"), []byte("3g2c"), []byte("KDDI"),
)
// AMp4 matches an audio MP4 file.
AMp4 = ftyp(
}
// AMp4 matches an audio MP4 file.
func AMp4(raw []byte, _ uint32) bool {
return ftyp(raw,
// audio for Adobe Flash Player 9+
[]byte("F4A "), []byte("F4B "),
// Apple iTunes AAC-LC (.M4A) Audio
@ -31,33 +40,61 @@ var (
// Nero Digital AAC Audio
[]byte("NDAS"),
)
// Mqv matches a Sony / Mobile QuickTime file.
Mqv = ftyp([]byte("mqt "))
// M4a matches an audio M4A file.
M4a = ftyp([]byte("M4A "))
// M4v matches an Appl4 M4V video file.
M4v = ftyp([]byte("M4V "), []byte("M4VH"), []byte("M4VP"))
// Heic matches a High Efficiency Image Coding (HEIC) file.
Heic = ftyp([]byte("heic"), []byte("heix"))
// HeicSequence matches a High Efficiency Image Coding (HEIC) file sequence.
HeicSequence = ftyp([]byte("hevc"), []byte("hevx"))
// Heif matches a High Efficiency Image File Format (HEIF) file.
Heif = ftyp([]byte("mif1"), []byte("heim"), []byte("heis"), []byte("avic"))
// HeifSequence matches a High Efficiency Image File Format (HEIF) file sequence.
HeifSequence = ftyp([]byte("msf1"), []byte("hevm"), []byte("hevs"), []byte("avcs"))
// Mj2 matches a Motion JPEG 2000 file: https://en.wikipedia.org/wiki/Motion_JPEG_2000.
Mj2 = ftyp([]byte("mj2s"), []byte("mjp2"), []byte("MFSM"), []byte("MGSV"))
// Dvb matches a Digital Video Broadcasting file: https://dvb.org.
// https://cconcolato.github.io/mp4ra/filetype.html
// https://github.com/file/file/blob/512840337ead1076519332d24fefcaa8fac36e06/magic/Magdir/animation#L135-L154
Dvb = ftyp(
}
// Mqv matches a Sony / Mobile QuickTime file.
func Mqv(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("mqt "))
}
// M4a matches an audio M4A file.
func M4a(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("M4A "))
}
// M4v matches an Appl4 M4V video file.
func M4v(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("M4V "), []byte("M4VH"), []byte("M4VP"))
}
// Heic matches a High Efficiency Image Coding (HEIC) file.
func Heic(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("heic"), []byte("heix"))
}
// HeicSequence matches a High Efficiency Image Coding (HEIC) file sequence.
func HeicSequence(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("hevc"), []byte("hevx"))
}
// Heif matches a High Efficiency Image File Format (HEIF) file.
func Heif(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("mif1"), []byte("heim"), []byte("heis"), []byte("avic"))
}
// HeifSequence matches a High Efficiency Image File Format (HEIF) file sequence.
func HeifSequence(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("msf1"), []byte("hevm"), []byte("hevs"), []byte("avcs"))
}
// Mj2 matches a Motion JPEG 2000 file: https://en.wikipedia.org/wiki/Motion_JPEG_2000.
func Mj2(raw []byte, _ uint32) bool {
return ftyp(raw, []byte("mj2s"), []byte("mjp2"), []byte("MFSM"), []byte("MGSV"))
}
// Dvb matches a Digital Video Broadcasting file: https://dvb.org.
// https://cconcolato.github.io/mp4ra/filetype.html
// https://github.com/file/file/blob/512840337ead1076519332d24fefcaa8fac36e06/magic/Magdir/animation#L135-L154
func Dvb(raw []byte, _ uint32) bool {
return ftyp(raw,
[]byte("dby1"), []byte("dsms"), []byte("dts1"), []byte("dts2"),
[]byte("dts3"), []byte("dxo "), []byte("dmb1"), []byte("dmpf"),
[]byte("drc1"), []byte("dv1a"), []byte("dv1b"), []byte("dv2a"),
[]byte("dv2b"), []byte("dv3a"), []byte("dv3b"), []byte("dvr1"),
[]byte("dvt1"), []byte("emsg"))
// TODO: add support for remaining video formats at ftyps.com.
)
}
// TODO: add support for remaining video formats at ftyps.com.
// QuickTime matches a QuickTime File Format file.
// https://www.loc.gov/preservation/digital/formats/fdd/fdd000052.shtml

View file

@ -2,56 +2,118 @@ package magic
import "bytes"
var (
// Png matches a Portable Network Graphics file.
// https://www.w3.org/TR/PNG/
Png = prefix([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A})
// Apng matches an Animated Portable Network Graphics file.
// https://wiki.mozilla.org/APNG_Specification
Apng = offset([]byte("acTL"), 37)
// Jpg matches a Joint Photographic Experts Group file.
Jpg = prefix([]byte{0xFF, 0xD8, 0xFF})
// Jp2 matches a JPEG 2000 Image file (ISO 15444-1).
Jp2 = jpeg2k([]byte{0x6a, 0x70, 0x32, 0x20})
// Jpx matches a JPEG 2000 Image file (ISO 15444-2).
Jpx = jpeg2k([]byte{0x6a, 0x70, 0x78, 0x20})
// Jpm matches a JPEG 2000 Image file (ISO 15444-6).
Jpm = jpeg2k([]byte{0x6a, 0x70, 0x6D, 0x20})
// Gif matches a Graphics Interchange Format file.
Gif = prefix([]byte("GIF87a"), []byte("GIF89a"))
// Bmp matches a bitmap image file.
Bmp = prefix([]byte{0x42, 0x4D})
// Ps matches a PostScript file.
Ps = prefix([]byte("%!PS-Adobe-"))
// Psd matches a Photoshop Document file.
Psd = prefix([]byte("8BPS"))
// Ico matches an ICO file.
Ico = prefix([]byte{0x00, 0x00, 0x01, 0x00}, []byte{0x00, 0x00, 0x02, 0x00})
// Icns matches an ICNS (Apple Icon Image format) file.
Icns = prefix([]byte("icns"))
// Tiff matches a Tagged Image File Format file.
Tiff = prefix([]byte{0x49, 0x49, 0x2A, 0x00}, []byte{0x4D, 0x4D, 0x00, 0x2A})
// Bpg matches a Better Portable Graphics file.
Bpg = prefix([]byte{0x42, 0x50, 0x47, 0xFB})
// Xcf matches GIMP image data.
Xcf = prefix([]byte("gimp xcf"))
// Pat matches GIMP pattern data.
Pat = offset([]byte("GPAT"), 20)
// Gbr matches GIMP brush data.
Gbr = offset([]byte("GIMP"), 20)
// Hdr matches Radiance HDR image.
// https://web.archive.org/web/20060913152809/http://local.wasp.uwa.edu.au/~pbourke/dataformats/pic/
Hdr = prefix([]byte("#?RADIANCE\n"))
// Xpm matches X PixMap image data.
Xpm = prefix([]byte{0x2F, 0x2A, 0x20, 0x58, 0x50, 0x4D, 0x20, 0x2A, 0x2F})
// Jxs matches a JPEG XS coded image file (ISO/IEC 21122-3).
Jxs = prefix([]byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x53, 0x20, 0x0D, 0x0A, 0x87, 0x0A})
// Jxr matches Microsoft HD JXR photo file.
Jxr = prefix([]byte{0x49, 0x49, 0xBC, 0x01})
)
// Png matches a Portable Network Graphics file.
// https://www.w3.org/TR/PNG/
func Png(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A})
}
func jpeg2k(sig []byte) Detector {
return func(raw []byte, _ uint32) bool {
// Apng matches an Animated Portable Network Graphics file.
// https://wiki.mozilla.org/APNG_Specification
func Apng(raw []byte, _ uint32) bool {
return offset(raw, []byte("acTL"), 37)
}
// Jpg matches a Joint Photographic Experts Group file.
func Jpg(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0xFF, 0xD8, 0xFF})
}
// Jp2 matches a JPEG 2000 Image file (ISO 15444-1).
func Jp2(raw []byte, _ uint32) bool {
return jpeg2k(raw, []byte{0x6a, 0x70, 0x32, 0x20})
}
// Jpx matches a JPEG 2000 Image file (ISO 15444-2).
func Jpx(raw []byte, _ uint32) bool {
return jpeg2k(raw, []byte{0x6a, 0x70, 0x78, 0x20})
}
// Jpm matches a JPEG 2000 Image file (ISO 15444-6).
func Jpm(raw []byte, _ uint32) bool {
return jpeg2k(raw, []byte{0x6a, 0x70, 0x6D, 0x20})
}
// Gif matches a Graphics Interchange Format file.
func Gif(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("GIF87a")) ||
bytes.HasPrefix(raw, []byte("GIF89a"))
}
// Bmp matches a bitmap image file.
func Bmp(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x42, 0x4D})
}
// Ps matches a PostScript file.
func Ps(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("%!PS-Adobe-"))
}
// Psd matches a Photoshop Document file.
func Psd(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("8BPS"))
}
// Ico matches an ICO file.
func Ico(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x00, 0x00, 0x01, 0x00}) ||
bytes.HasPrefix(raw, []byte{0x00, 0x00, 0x02, 0x00})
}
// Icns matches an ICNS (Apple Icon Image format) file.
func Icns(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("icns"))
}
// Tiff matches a Tagged Image File Format file.
func Tiff(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x49, 0x49, 0x2A, 0x00}) ||
bytes.HasPrefix(raw, []byte{0x4D, 0x4D, 0x00, 0x2A})
}
// Bpg matches a Better Portable Graphics file.
func Bpg(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x42, 0x50, 0x47, 0xFB})
}
// Xcf matches GIMP image data.
func Xcf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("gimp xcf"))
}
// Pat matches GIMP pattern data.
func Pat(raw []byte, _ uint32) bool {
return offset(raw, []byte("GPAT"), 20)
}
// Gbr matches GIMP brush data.
func Gbr(raw []byte, _ uint32) bool {
return offset(raw, []byte("GIMP"), 20)
}
// Hdr matches Radiance HDR image.
// https://web.archive.org/web/20060913152809/http://local.wasp.uwa.edu.au/~pbourke/dataformats/pic/
func Hdr(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("#?RADIANCE\n"))
}
// Xpm matches X PixMap image data.
func Xpm(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x2F, 0x2A, 0x20, 0x58, 0x50, 0x4D, 0x20, 0x2A, 0x2F})
}
// Jxs matches a JPEG XS coded image file (ISO/IEC 21122-3).
func Jxs(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x53, 0x20, 0x0D, 0x0A, 0x87, 0x0A})
}
// Jxr matches Microsoft HD JXR photo file.
func Jxr(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x49, 0x49, 0xBC, 0x01})
}
func jpeg2k(raw []byte, sig []byte) bool {
if len(raw) < 24 {
return false
}
@ -61,7 +123,6 @@ func jpeg2k(sig []byte) Detector {
return false
}
return bytes.Equal(raw[20:24], sig)
}
}
// Webp matches a WebP file.
@ -108,3 +169,20 @@ func Jxl(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0xFF, 0x0A}) ||
bytes.HasPrefix(raw, []byte("\x00\x00\x00\x0cJXL\x20\x0d\x0a\x87\x0a"))
}
// DXF matches Drawing Exchange Format AutoCAD file.
// There does not seem to be a clear specification and the files in the wild
// differ wildly.
// https://images.autodesk.com/adsk/files/autocad_2012_pdf_dxf-reference_enu.pdf
//
// I collected these signatures by downloading a few dozen files from
// http://cd.textfiles.com/amigaenv/DXF/OBJEKTE/ and
// https://sembiance.com/fileFormatSamples/poly/dxf/ and then
// xxd -l 16 {} | sort | uniq.
// These signatures are only for the ASCII version of DXF. There is a binary version too.
func DXF(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte(" 0\x0ASECTION\x0A")) ||
bytes.HasPrefix(raw, []byte(" 0\x0D\x0ASECTION\x0D\x0A")) ||
bytes.HasPrefix(raw, []byte("0\x0ASECTION\x0A")) ||
bytes.HasPrefix(raw, []byte("0\x0D\x0ASECTION\x0D\x0A"))
}

View file

@ -3,7 +3,6 @@ package magic
import (
"bytes"
"fmt"
"github.com/gabriel-vasile/mimetype/internal/scan"
)
@ -22,37 +21,20 @@ type (
}
)
// prefix creates a Detector which returns true if any of the provided signatures
// is the prefix of the raw input.
func prefix(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool {
for _, s := range sigs {
if bytes.HasPrefix(raw, s) {
return true
}
}
return false
}
}
// offset creates a Detector which returns true if the provided signature can be
// offset returns true if the provided signature can be
// found at offset in the raw input.
func offset(sig []byte, offset int) Detector {
return func(raw []byte, limit uint32) bool {
func offset(raw []byte, sig []byte, offset int) bool {
return len(raw) > offset && bytes.HasPrefix(raw[offset:], sig)
}
}
// ciPrefix is like prefix but the check is case insensitive.
func ciPrefix(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool {
func ciPrefix(raw []byte, sigs ...[]byte) bool {
for _, s := range sigs {
if ciCheck(s, raw) {
return true
}
}
return false
}
}
func ciCheck(sig, raw []byte) bool {
if len(raw) < len(sig)+1 {
@ -72,11 +54,8 @@ func ciCheck(sig, raw []byte) bool {
return true
}
// xml creates a Detector which returns true if any of the provided XML signatures
// matches the raw input.
func xml(sigs ...xmlSig) Detector {
return func(raw []byte, limit uint32) bool {
b := scan.Bytes(raw)
// xml returns true if any of the provided XML signatures matches the raw input.
func xml(b scan.Bytes, sigs ...xmlSig) bool {
b.TrimLWS()
if len(b) == 0 {
return false
@ -87,7 +66,6 @@ func xml(sigs ...xmlSig) Detector {
}
}
return false
}
}
func xmlCheck(sig xmlSig, raw []byte) bool {
raw = raw[:min(len(raw), 512)]
@ -103,11 +81,8 @@ func xmlCheck(sig xmlSig, raw []byte) bool {
return localNameIndex != -1 && localNameIndex < bytes.Index(raw, sig.xmlns)
}
// markup creates a Detector which returns true is any of the HTML signatures
// matches the raw input.
func markup(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool {
b := scan.Bytes(raw)
// markup returns true is any of the HTML signatures matches the raw input.
func markup(b scan.Bytes, sigs ...[]byte) bool {
if bytes.HasPrefix(b, []byte{0xEF, 0xBB, 0xBF}) {
// We skip the UTF-8 BOM if present to ensure we correctly
// process any leading whitespace. The presence of the BOM
@ -124,7 +99,6 @@ func markup(sigs ...[]byte) Detector {
}
}
return false
}
}
func markupCheck(sig, raw []byte) bool {
if len(raw) < len(sig)+1 {
@ -149,10 +123,8 @@ func markupCheck(sig, raw []byte) bool {
return true
}
// ftyp creates a Detector which returns true if any of the FTYP signatures
// matches the raw input.
func ftyp(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool {
// ftyp returns true if any of the FTYP signatures matches the raw input.
func ftyp(raw []byte, sigs ...[]byte) bool {
if len(raw) < 12 {
return false
}
@ -162,16 +134,6 @@ func ftyp(sigs ...[]byte) Detector {
}
}
return false
}
}
func newXMLSig(localName, xmlns string) xmlSig {
ret := xmlSig{xmlns: []byte(xmlns)}
if localName != "" {
ret.localName = []byte(fmt.Sprintf("<%s", localName))
}
return ret
}
// A valid shebang starts with the "#!" characters,
@ -184,29 +146,17 @@ func newXMLSig(localName, xmlns string) xmlSig {
// #! /usr/bin/env php
//
// /usr/bin/env is the interpreter, php is the first and only argument.
func shebang(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool {
b := scan.Bytes(raw)
func shebang(b scan.Bytes, matchFlags scan.Flags, sigs ...[]byte) bool {
line := b.Line()
if len(line) < 2 || line[0] != '#' || line[1] != '!' {
return false
}
line = line[2:]
line.TrimLWS()
for _, s := range sigs {
if shebangCheck(s, line) {
if line.Match(s, matchFlags) != -1 {
return true
}
}
return false
}
}
func shebangCheck(sig []byte, raw scan.Bytes) bool {
if len(raw) < len(sig)+2 {
return false
}
if raw[0] != '#' || raw[1] != '!' {
return false
}
raw.Advance(2) // skip #! we checked above
raw.TrimLWS()
raw.TrimRWS()
return bytes.Equal(raw, sig)
}

View file

@ -44,17 +44,6 @@ func Ole(raw []byte, limit uint32) bool {
return bytes.HasPrefix(raw, []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1})
}
// Aaf matches an Advanced Authoring Format file.
// See: https://pyaaf.readthedocs.io/en/latest/about.html
// See: https://en.wikipedia.org/wiki/Advanced_Authoring_Format
func Aaf(raw []byte, limit uint32) bool {
if len(raw) < 31 {
return false
}
return bytes.HasPrefix(raw[8:], []byte{0x41, 0x41, 0x46, 0x42, 0x0D, 0x00, 0x4F, 0x4D}) &&
(raw[30] == 0x09 || raw[30] == 0x0C)
}
// Doc matches a Microsoft Word 97-2003 file.
// See: https://github.com/decalage2/oletools/blob/412ee36ae45e70f42123e835871bac956d958461/oletools/common/clsid.py
func Doc(raw []byte, _ uint32) bool {
@ -209,3 +198,14 @@ func matchOleClsid(in []byte, clsid []byte) bool {
return bytes.HasPrefix(in[clsidOffset:], clsid)
}
// WPD matches a WordPerfect document.
func WPD(raw []byte, _ uint32) bool {
if len(raw) < 10 {
return false
}
if !bytes.HasPrefix(raw, []byte("\xffWPC")) {
return false
}
return raw[8] == 1 && raw[9] == 10
}

View file

@ -10,9 +10,9 @@ import (
"github.com/gabriel-vasile/mimetype/internal/scan"
)
var (
// HTML matches a Hypertext Markup Language file.
HTML = markup(
// HTML matches a Hypertext Markup Language file.
func HTML(raw []byte, _ uint32) bool {
return markup(raw,
[]byte("<!DOCTYPE HTML"),
[]byte("<HTML"),
[]byte("<HEAD"),
@ -31,133 +31,254 @@ var (
[]byte("<P"),
[]byte("<!--"),
)
// XML matches an Extensible Markup Language file.
XML = markup([]byte("<?XML"))
// Owl2 matches an Owl ontology file.
Owl2 = xml(newXMLSig("Ontology", `xmlns="http://www.w3.org/2002/07/owl#"`))
// Rss matches a Rich Site Summary file.
Rss = xml(newXMLSig("rss", ""))
// Atom matches an Atom Syndication Format file.
Atom = xml(newXMLSig("feed", `xmlns="http://www.w3.org/2005/Atom"`))
// Kml matches a Keyhole Markup Language file.
Kml = xml(
newXMLSig("kml", `xmlns="http://www.opengis.net/kml/2.2"`),
newXMLSig("kml", `xmlns="http://earth.google.com/kml/2.0"`),
newXMLSig("kml", `xmlns="http://earth.google.com/kml/2.1"`),
newXMLSig("kml", `xmlns="http://earth.google.com/kml/2.2"`),
}
// XML matches an Extensible Markup Language file.
func XML(raw []byte, _ uint32) bool {
return markup(raw, []byte("<?XML"))
}
// Owl2 matches an Owl ontology file.
func Owl2(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<Ontology"), []byte(`xmlns="http://www.w3.org/2002/07/owl#"`)},
)
// Xliff matches a XML Localization Interchange File Format file.
Xliff = xml(newXMLSig("xliff", `xmlns="urn:oasis:names:tc:xliff:document:1.2"`))
// Collada matches a COLLAborative Design Activity file.
Collada = xml(newXMLSig("COLLADA", `xmlns="http://www.collada.org/2005/11/COLLADASchema"`))
// Gml matches a Geography Markup Language file.
Gml = xml(
newXMLSig("", `xmlns:gml="http://www.opengis.net/gml"`),
newXMLSig("", `xmlns:gml="http://www.opengis.net/gml/3.2"`),
newXMLSig("", `xmlns:gml="http://www.opengis.net/gml/3.3/exr"`),
}
// Rss matches a Rich Site Summary file.
func Rss(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<rss"), []byte{}},
)
// Gpx matches a GPS Exchange Format file.
Gpx = xml(newXMLSig("gpx", `xmlns="http://www.topografix.com/GPX/1/1"`))
// Tcx matches a Training Center XML file.
Tcx = xml(newXMLSig("TrainingCenterDatabase", `xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"`))
// X3d matches an Extensible 3D Graphics file.
X3d = xml(newXMLSig("X3D", `xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance"`))
// Amf matches an Additive Manufacturing XML file.
Amf = xml(newXMLSig("amf", ""))
// Threemf matches a 3D Manufacturing Format file.
Threemf = xml(newXMLSig("model", `xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"`))
// Xfdf matches a XML Forms Data Format file.
Xfdf = xml(newXMLSig("xfdf", `xmlns="http://ns.adobe.com/xfdf/"`))
// VCard matches a Virtual Contact File.
VCard = ciPrefix([]byte("BEGIN:VCARD\n"), []byte("BEGIN:VCARD\r\n"))
// ICalendar matches a iCalendar file.
ICalendar = ciPrefix([]byte("BEGIN:VCALENDAR\n"), []byte("BEGIN:VCALENDAR\r\n"))
phpPageF = ciPrefix(
}
// Atom matches an Atom Syndication Format file.
func Atom(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<feed"), []byte(`xmlns="http://www.w3.org/2005/Atom"`)},
)
}
// Kml matches a Keyhole Markup Language file.
func Kml(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<kml"), []byte(`xmlns="http://www.opengis.net/kml/2.2"`)},
xmlSig{[]byte("<kml"), []byte(`xmlns="http://earth.google.com/kml/2.0"`)},
xmlSig{[]byte("<kml"), []byte(`xmlns="http://earth.google.com/kml/2.1"`)},
xmlSig{[]byte("<kml"), []byte(`xmlns="http://earth.google.com/kml/2.2"`)},
)
}
// Xliff matches a XML Localization Interchange File Format file.
func Xliff(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<xliff"), []byte(`xmlns="urn:oasis:names:tc:xliff:document:1.2"`)},
)
}
// Collada matches a COLLAborative Design Activity file.
func Collada(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<COLLADA"), []byte(`xmlns="http://www.collada.org/2005/11/COLLADASchema"`)},
)
}
// Gml matches a Geography Markup Language file.
func Gml(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte{}, []byte(`xmlns:gml="http://www.opengis.net/gml"`)},
xmlSig{[]byte{}, []byte(`xmlns:gml="http://www.opengis.net/gml/3.2"`)},
xmlSig{[]byte{}, []byte(`xmlns:gml="http://www.opengis.net/gml/3.3/exr"`)},
)
}
// Gpx matches a GPS Exchange Format file.
func Gpx(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<gpx"), []byte(`xmlns="http://www.topografix.com/GPX/1/1"`)},
)
}
// Tcx matches a Training Center XML file.
func Tcx(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<TrainingCenterDatabase"), []byte(`xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"`)},
)
}
// X3d matches an Extensible 3D Graphics file.
func X3d(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<X3D"), []byte(`xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance"`)},
)
}
// Amf matches an Additive Manufacturing XML file.
func Amf(raw []byte, _ uint32) bool {
return xml(raw, xmlSig{[]byte("<amf"), []byte{}})
}
// Threemf matches a 3D Manufacturing Format file.
func Threemf(raw []byte, _ uint32) bool {
return xml(raw,
xmlSig{[]byte("<model"), []byte(`xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"`)},
)
}
// Xfdf matches a XML Forms Data Format file.
func Xfdf(raw []byte, _ uint32) bool {
return xml(raw, xmlSig{[]byte("<xfdf"), []byte(`xmlns="http://ns.adobe.com/xfdf/"`)})
}
// VCard matches a Virtual Contact File.
func VCard(raw []byte, _ uint32) bool {
return ciPrefix(raw, []byte("BEGIN:VCARD\n"), []byte("BEGIN:VCARD\r\n"))
}
// ICalendar matches a iCalendar file.
func ICalendar(raw []byte, _ uint32) bool {
return ciPrefix(raw, []byte("BEGIN:VCALENDAR\n"), []byte("BEGIN:VCALENDAR\r\n"))
}
func phpPageF(raw []byte, _ uint32) bool {
return ciPrefix(raw,
[]byte("<?PHP"),
[]byte("<?\n"),
[]byte("<?\r"),
[]byte("<? "),
)
phpScriptF = shebang(
}
func phpScriptF(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS,
[]byte("/usr/local/bin/php"),
[]byte("/usr/bin/php"),
[]byte("/usr/bin/env php"),
[]byte("/usr/bin/env -S php"),
)
// Js matches a Javascript file.
Js = shebang(
}
// Js matches a Javascript file.
func Js(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS,
[]byte("/bin/node"),
[]byte("/usr/bin/node"),
[]byte("/bin/nodejs"),
[]byte("/usr/bin/nodejs"),
[]byte("/usr/bin/env node"),
[]byte("/usr/bin/env -S node"),
[]byte("/usr/bin/env nodejs"),
[]byte("/usr/bin/env -S nodejs"),
)
// Lua matches a Lua programming language file.
Lua = shebang(
}
// Lua matches a Lua programming language file.
func Lua(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS|scan.FullWord,
[]byte("/usr/bin/lua"),
[]byte("/usr/local/bin/lua"),
[]byte("/usr/bin/env lua"),
[]byte("/usr/bin/env -S lua"),
)
// Perl matches a Perl programming language file.
Perl = shebang(
}
// Perl matches a Perl programming language file.
func Perl(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS|scan.FullWord,
[]byte("/usr/bin/perl"),
[]byte("/usr/bin/env perl"),
[]byte("/usr/bin/env -S perl"),
)
// Python matches a Python programming language file.
Python = shebang(
}
// Python matches a Python programming language file.
func Python(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS,
[]byte("/usr/bin/python"),
[]byte("/usr/local/bin/python"),
[]byte("/usr/bin/env python"),
[]byte("/usr/bin/env -S python"),
[]byte("/usr/bin/python2"),
[]byte("/usr/local/bin/python2"),
[]byte("/usr/bin/env python2"),
[]byte("/usr/bin/env -S python2"),
[]byte("/usr/bin/python3"),
[]byte("/usr/local/bin/python3"),
[]byte("/usr/bin/env python3"),
[]byte("/usr/bin/env -S python3"),
)
// Ruby matches a Ruby programming language file.
Ruby = shebang(
}
// Ruby matches a Ruby programming language file.
func Ruby(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS,
[]byte("/usr/bin/ruby"),
[]byte("/usr/local/bin/ruby"),
[]byte("/usr/bin/env ruby"),
[]byte("/usr/bin/env -S ruby"),
)
// Tcl matches a Tcl programming language file.
Tcl = shebang(
}
// Tcl matches a Tcl programming language file.
func Tcl(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS,
[]byte("/usr/bin/tcl"),
[]byte("/usr/local/bin/tcl"),
[]byte("/usr/bin/env tcl"),
[]byte("/usr/bin/env -S tcl"),
[]byte("/usr/bin/tclsh"),
[]byte("/usr/local/bin/tclsh"),
[]byte("/usr/bin/env tclsh"),
[]byte("/usr/bin/env -S tclsh"),
[]byte("/usr/bin/wish"),
[]byte("/usr/local/bin/wish"),
[]byte("/usr/bin/env wish"),
[]byte("/usr/bin/env -S wish"),
)
// Rtf matches a Rich Text Format file.
Rtf = prefix([]byte("{\\rtf"))
// Shell matches a shell script file.
Shell = shebang(
}
// Rtf matches a Rich Text Format file.
func Rtf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("{\\rtf"))
}
// Shell matches a shell script file.
func Shell(raw []byte, _ uint32) bool {
return shebang(raw,
scan.CompactWS|scan.FullWord,
[]byte("/bin/sh"),
[]byte("/bin/bash"),
[]byte("/usr/local/bin/bash"),
[]byte("/usr/bin/env bash"),
[]byte("/usr/bin/env -S bash"),
[]byte("/bin/csh"),
[]byte("/usr/local/bin/csh"),
[]byte("/usr/bin/env csh"),
[]byte("/usr/bin/env -S csh"),
[]byte("/bin/dash"),
[]byte("/usr/local/bin/dash"),
[]byte("/usr/bin/env dash"),
[]byte("/usr/bin/env -S dash"),
[]byte("/bin/ksh"),
[]byte("/usr/local/bin/ksh"),
[]byte("/usr/bin/env ksh"),
[]byte("/usr/bin/env -S ksh"),
[]byte("/bin/tcsh"),
[]byte("/usr/local/bin/tcsh"),
[]byte("/usr/bin/env tcsh"),
[]byte("/usr/bin/env -S tcsh"),
[]byte("/bin/zsh"),
[]byte("/usr/local/bin/zsh"),
[]byte("/usr/bin/env zsh"),
[]byte("/usr/bin/env -S zsh"),
)
)
}
// Text matches a plain text file.
//
@ -183,10 +304,14 @@ func Text(raw []byte, _ uint32) bool {
// XHTML matches an XHTML file. This check depends on the XML check to have passed.
func XHTML(raw []byte, limit uint32) bool {
raw = raw[:min(len(raw), 4096)]
raw = raw[:min(len(raw), 1024)]
b := scan.Bytes(raw)
return b.Search([]byte("<!DOCTYPE HTML"), scan.CompactWS|scan.IgnoreCase) != -1 ||
b.Search([]byte("<HTML XMLNS="), scan.CompactWS|scan.IgnoreCase) != -1
i, _ := b.Search([]byte("<!DOCTYPE HTML"), scan.CompactWS|scan.IgnoreCase)
if i != -1 {
return true
}
i, _ = b.Search([]byte("<HTML XMLNS="), scan.CompactWS|scan.IgnoreCase)
return i != -1
}
// Php matches a PHP: Hypertext Preprocessor file.
@ -294,11 +419,12 @@ func svgWithoutXMLDeclaration(s scan.Bytes) bool {
return false
}
targetName, targetVal := "xmlns", "http://www.w3.org/2000/svg"
aName, aVal, hasMore := "", "", true
targetName, targetVal := []byte("xmlns"), []byte("http://www.w3.org/2000/svg")
var aName, aVal []byte
hasMore := true
for hasMore {
aName, aVal, hasMore = mkup.GetAnAttribute(&s)
if aName == targetName && aVal == targetVal {
if bytes.Equal(aName, targetName) && bytes.Equal(aVal, targetVal) {
return true
}
if !hasMore {
@ -325,10 +451,11 @@ func svgWithXMLDeclaration(s scan.Bytes) bool {
// version is a required attribute for XML.
hasVersion := false
aName, hasMore := "", true
var aName []byte
hasMore := true
for hasMore {
aName, _, hasMore = mkup.GetAnAttribute(&s)
if aName == "version" {
if bytes.Equal(aName, []byte("version")) {
hasVersion = true
break
}

View file

@ -4,17 +4,23 @@ import (
"bytes"
)
var (
// Flv matches a Flash video file.
Flv = prefix([]byte("\x46\x4C\x56\x01"))
// Asf matches an Advanced Systems Format file.
Asf = prefix([]byte{
// Flv matches a Flash video file.
func Flv(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("\x46\x4C\x56\x01"))
}
// Asf matches an Advanced Systems Format file.
func Asf(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C,
})
// Rmvb matches a RealMedia Variable Bitrate file.
Rmvb = prefix([]byte{0x2E, 0x52, 0x4D, 0x46})
)
}
// Rmvb matches a RealMedia Variable Bitrate file.
func Rmvb(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte{0x2E, 0x52, 0x4D, 0x46})
}
// WebM matches a WebM file.
func WebM(raw []byte, limit uint32) bool {

View file

@ -6,32 +6,65 @@ import (
"github.com/gabriel-vasile/mimetype/internal/scan"
)
var (
// Odt matches an OpenDocument Text file.
Odt = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.text"), 30)
// Ott matches an OpenDocument Text Template file.
Ott = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.text-template"), 30)
// Ods matches an OpenDocument Spreadsheet file.
Ods = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.spreadsheet"), 30)
// Ots matches an OpenDocument Spreadsheet Template file.
Ots = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.spreadsheet-template"), 30)
// Odp matches an OpenDocument Presentation file.
Odp = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.presentation"), 30)
// Otp matches an OpenDocument Presentation Template file.
Otp = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.presentation-template"), 30)
// Odg matches an OpenDocument Drawing file.
Odg = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.graphics"), 30)
// Otg matches an OpenDocument Drawing Template file.
Otg = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.graphics-template"), 30)
// Odf matches an OpenDocument Formula file.
Odf = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.formula"), 30)
// Odc matches an OpenDocument Chart file.
Odc = offset([]byte("mimetypeapplication/vnd.oasis.opendocument.chart"), 30)
// Epub matches an EPUB file.
Epub = offset([]byte("mimetypeapplication/epub+zip"), 30)
// Sxc matches an OpenOffice Spreadsheet file.
Sxc = offset([]byte("mimetypeapplication/vnd.sun.xml.calc"), 30)
)
// Odt matches an OpenDocument Text file.
func Odt(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.text"), 30)
}
// Ott matches an OpenDocument Text Template file.
func Ott(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.text-template"), 30)
}
// Ods matches an OpenDocument Spreadsheet file.
func Ods(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.spreadsheet"), 30)
}
// Ots matches an OpenDocument Spreadsheet Template file.
func Ots(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.spreadsheet-template"), 30)
}
// Odp matches an OpenDocument Presentation file.
func Odp(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.presentation"), 30)
}
// Otp matches an OpenDocument Presentation Template file.
func Otp(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.presentation-template"), 30)
}
// Odg matches an OpenDocument Drawing file.
func Odg(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.graphics"), 30)
}
// Otg matches an OpenDocument Drawing Template file.
func Otg(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.graphics-template"), 30)
}
// Odf matches an OpenDocument Formula file.
func Odf(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.formula"), 30)
}
// Odc matches an OpenDocument Chart file.
func Odc(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.oasis.opendocument.chart"), 30)
}
// Epub matches an EPUB file.
func Epub(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/epub+zip"), 30)
}
// Sxc matches an OpenOffice Spreadsheet file.
func Sxc(raw []byte, _ uint32) bool {
return offset(raw, []byte("mimetypeapplication/vnd.sun.xml.calc"), 30)
}
// Zip matches a zip archive.
func Zip(raw []byte, limit uint32) bool {
@ -134,11 +167,11 @@ func msoxml(raw scan.Bytes, searchFor zipEntries, stopAfter int) bool {
// If the first is not one of the next usually expected entries,
// then abort this check.
if i == 0 {
if !bytes.Equal(f, []byte("[Content_Types].xml")) &&
!bytes.Equal(f, []byte("_rels/.rels")) &&
!bytes.Equal(f, []byte("docProps")) &&
!bytes.Equal(f, []byte("customXml")) &&
!bytes.Equal(f, []byte("[trash]")) {
if !bytes.Equal(f, []byte("[Content_Types].xml")) && // this is a file
!bytes.HasPrefix(f, []byte("_rels/")) && // these are directories
!bytes.HasPrefix(f, []byte("docProps/")) &&
!bytes.HasPrefix(f, []byte("customXml/")) &&
!bytes.HasPrefix(f, []byte("[trash]/")) {
return false
}
}

View file

@ -8,46 +8,48 @@ import (
"github.com/gabriel-vasile/mimetype/internal/scan"
)
func GetAnAttribute(s *scan.Bytes) (name, val string, hasMore bool) {
// GetAnAttribute assumes we passed over an SGML tag and extracts first
// attribute and its value.
//
// Initially, this code existed inside charset/charset.go, because it was part of
// implementing the https://html.spec.whatwg.org/multipage/parsing.html#prescan-a-byte-stream-to-determine-its-encoding
// algorithm. But because extracting an attribute from a tag is the same for
// both HTML and XML, then the code was moved here.
func GetAnAttribute(s *scan.Bytes) (name, val []byte, hasMore bool) {
for scan.ByteIsWS(s.Peek()) || s.Peek() == '/' {
s.Advance(1)
}
if s.Peek() == '>' {
return "", "", false
return nil, nil, false
}
// Allocate 10 to avoid resizes.
// Attribute names and values are continuous slices of bytes in input,
// so we could do without allocating and returning slices of input.
nameB := make([]byte, 0, 10)
origS, end := *s, 0
// step 4 and 5
for {
// bap means byte at position in the specification.
bap := s.Pop()
if bap == 0 {
return "", "", false
return nil, nil, false
}
if bap == '=' && len(nameB) > 0 {
if bap == '=' && end > 0 {
val, hasMore := getAValue(s)
return string(nameB), string(val), hasMore
return origS[:end], val, hasMore
} else if scan.ByteIsWS(bap) {
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
if s.Peek() != '=' {
return string(nameB), "", true
return origS[:end], nil, true
}
s.Advance(1)
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
val, hasMore := getAValue(s)
return string(nameB), string(val), hasMore
return origS[:end], val, hasMore
} else if bap == '/' || bap == '>' {
return string(nameB), "", false
} else if bap >= 'A' && bap <= 'Z' {
nameB = append(nameB, bap+0x20)
} else {
nameB = append(nameB, bap)
return origS[:end], nil, false
} else { // for any ASCII, non-ASCII, just advance
end++
}
}
}

View file

@ -138,46 +138,85 @@ func (b *Bytes) Uint16() (uint16, bool) {
return v, true
}
type Flags int
const (
CompactWS = 1 << iota
// CompactWS will make one whitespace from pattern to match one or more spaces from input.
CompactWS Flags = 1 << iota
// IgnoreCase will match lower case from pattern with lower case from input.
// IgnoreCase will match upper case from pattern with both lower and upper case from input.
// This flag is not really well named,
IgnoreCase
// FullWord ensures the input ends with a full word (it's followed by spaces.)
FullWord
)
// Search for occurences of pattern p inside b at any index.
func (b Bytes) Search(p []byte, flags int) int {
// It returns the index where p was found in b and how many bytes were needed
// for matching the pattern.
func (b Bytes) Search(p []byte, flags Flags) (i int, l int) {
lb, lp := len(b), len(p)
if lp == 0 {
return 0, 0
}
if lb == 0 {
return -1, 0
}
if flags == 0 {
return bytes.Index(b, p)
if i = bytes.Index(b, p); i == -1 {
return -1, 0
} else {
return i, lp
}
}
lb, lp := len(b), len(p)
for i := range b {
if lb-i < lp {
return -1
return -1, 0
}
if b[i:].Match(p, flags) {
return i
if l = b[i:].Match(p, flags); l != -1 {
return i, l
}
}
return 0
return -1, 0
}
// Match pattern p at index 0 of b.
func (b Bytes) Match(p []byte, flags int) bool {
for len(b) > 0 {
// If we finished all we we're looking for from p.
// Match returns how many bytes were needed to match pattern p.
// It returns -1 if p does not match b.
func (b Bytes) Match(p []byte, flags Flags) int {
l := len(b)
if len(p) == 0 {
return true
return 0
}
if l == 0 {
return -1
}
// If no flags, or scanning for full word at the end of pattern then
// do a fast HasPrefix check.
// For other flags it's not possible to use HasPrefix.
if flags == 0 || flags&FullWord > 0 {
if bytes.HasPrefix(b, p) {
b = b[len(p):]
p = p[len(p):]
goto out
}
return -1
}
for len(b) > 0 {
// If we finished all we were looking for from p.
if len(p) == 0 {
goto out
}
if flags&IgnoreCase > 0 && isUpper(p[0]) {
if upper(b[0]) != p[0] {
return false
return -1
}
b, p = b[1:], p[1:]
} else if flags&CompactWS > 0 && ByteIsWS(p[0]) {
p = p[1:]
if !ByteIsWS(b[0]) {
return false
return -1
}
b = b[1:]
if !ByteIsWS(p[0]) {
@ -185,12 +224,22 @@ func (b Bytes) Match(p []byte, flags int) bool {
}
} else {
if b[0] != p[0] {
return false
return -1
}
b, p = b[1:], p[1:]
}
}
return true
out:
// If p still has leftover characters, it means it didn't fully match b.
if len(p) > 0 {
return -1
}
if flags&FullWord > 0 {
if len(b) > 0 && !ByteIsWS(b[0]) {
return -1
}
}
return l - len(b)
}
func isUpper(c byte) bool {

View file

@ -2,6 +2,7 @@ package mimetype
import (
"mime"
"strings"
"github.com/gabriel-vasile/mimetype/internal/charset"
"github.com/gabriel-vasile/mimetype/internal/magic"
@ -109,7 +110,7 @@ func (m *MIME) match(in []byte, readLimit uint32) *MIME {
// Limit the number of bytes searched for to 1024.
charset = f(in[:min(len(in), 1024)])
}
if m == root {
if m == root || charset == "" {
return m
}
@ -126,6 +127,27 @@ func (m *MIME) flatten() []*MIME {
return out
}
// hierarchy returns an easy to read list of ancestors for m.
// For example, application/json would return json>txt>root.
func (m *MIME) hierarchy() string {
h := ""
for m := m; m != nil; m = m.Parent() {
e := strings.TrimPrefix(m.Extension(), ".")
if e == "" {
// There are some MIME without extensions. When generating the hierarchy,
// it would be confusing to use empty string as extension.
// Use the subtype instead; ex: application/x-executable -> x-executable.
e = strings.Split(m.String(), "/")[1]
if m.Is("application/octet-stream") {
// for octet-stream use root, because it's short and used in many places
e = "root"
}
}
h += ">" + e
}
return strings.TrimPrefix(h, ">")
}
// clone creates a new MIME with the provided optional MIME parameters.
func (m *MIME) clone(charset string) *MIME {
clonedMIME := m.mime
@ -155,7 +177,10 @@ func (m *MIME) cloneHierarchy(charset string) *MIME {
}
func (m *MIME) lookup(mime string) *MIME {
for _, n := range append(m.aliases, m.mime) {
if mime == m.mime {
return m
}
for _, n := range m.aliases {
if n == mime {
return m
}

View file

@ -12,7 +12,7 @@ import (
"sync/atomic"
)
var defaultLimit uint32 = 3072
const defaultLimit uint32 = 3072
// readLimit is the maximum number of bytes from the input used when detecting.
var readLimit uint32 = defaultLimit
@ -112,15 +112,18 @@ func SetLimit(limit uint32) {
}
// Extend adds detection for other file formats.
// It is equivalent to calling Extend() on the root mime type "application/octet-stream".
// It is equivalent to calling Extend() on the root MIME type "application/octet-stream".
func Extend(detector func(raw []byte, limit uint32) bool, mime, extension string, aliases ...string) {
root.Extend(detector, mime, extension, aliases...)
}
// Lookup finds a MIME object by its string representation.
// The representation can be the main mime type, or any of its aliases.
func Lookup(mime string) *MIME {
// The representation can be the main MIME type, or any of its aliases.
func Lookup(m string) *MIME {
// We store the MIME types without optional params, so
// perform parsing to extract the target MIME type without optional params.
m, _, _ = mime.ParseMediaType(m)
mu.RLock()
defer mu.RUnlock()
return root.lookup(mime)
return root.lookup(m)
}

View file

@ -1,196 +1,197 @@
## 191 Supported MIME types
## 192 Supported MIME types
This file is automatically generated when running tests. Do not edit manually.
Extension | MIME type | Aliases
--------- | --------- | -------
**n/a** | application/octet-stream | -
**.xpm** | image/x-xpixmap | -
**.7z** | application/x-7z-compressed | -
**.zip** | application/zip | application/x-zip, application/x-zip-compressed
**.docx** | application/vnd.openxmlformats-officedocument.wordprocessingml.document | -
**.pptx** | application/vnd.openxmlformats-officedocument.presentationml.presentation | -
**.xlsx** | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | -
**.epub** | application/epub+zip | -
**.apk** | application/vnd.android.package-archive | -
**.jar** | application/java-archive | application/jar, application/jar-archive, application/x-java-archive
**.odt** | application/vnd.oasis.opendocument.text | application/x-vnd.oasis.opendocument.text
**.ott** | application/vnd.oasis.opendocument.text-template | application/x-vnd.oasis.opendocument.text-template
**.ods** | application/vnd.oasis.opendocument.spreadsheet | application/x-vnd.oasis.opendocument.spreadsheet
**.ots** | application/vnd.oasis.opendocument.spreadsheet-template | application/x-vnd.oasis.opendocument.spreadsheet-template
**.odp** | application/vnd.oasis.opendocument.presentation | application/x-vnd.oasis.opendocument.presentation
**.otp** | application/vnd.oasis.opendocument.presentation-template | application/x-vnd.oasis.opendocument.presentation-template
**.odg** | application/vnd.oasis.opendocument.graphics | application/x-vnd.oasis.opendocument.graphics
**.otg** | application/vnd.oasis.opendocument.graphics-template | application/x-vnd.oasis.opendocument.graphics-template
**.odf** | application/vnd.oasis.opendocument.formula | application/x-vnd.oasis.opendocument.formula
**.odc** | application/vnd.oasis.opendocument.chart | application/x-vnd.oasis.opendocument.chart
**.sxc** | application/vnd.sun.xml.calc | -
**.kmz** | application/vnd.google-earth.kmz | -
**.vsdx** | application/vnd.ms-visio.drawing.main+xml | -
**.pdf** | application/pdf | application/x-pdf
**.fdf** | application/vnd.fdf | -
**n/a** | application/x-ole-storage | -
**.msi** | application/x-ms-installer | application/x-windows-installer, application/x-msi
**.aaf** | application/octet-stream | -
**.msg** | application/vnd.ms-outlook | -
**.xls** | application/vnd.ms-excel | application/msexcel
**.pub** | application/vnd.ms-publisher | -
**.ppt** | application/vnd.ms-powerpoint | application/mspowerpoint
**.doc** | application/msword | application/vnd.ms-word
**.ps** | application/postscript | -
**.psd** | image/vnd.adobe.photoshop | image/x-psd, application/photoshop
**.p7s** | application/pkcs7-signature | -
**.ogg** | application/ogg | application/x-ogg
**.oga** | audio/ogg | -
**.ogv** | video/ogg | -
**.png** | image/png | -
**.png** | image/vnd.mozilla.apng | -
**.jpg** | image/jpeg | -
**.jxl** | image/jxl | -
**.jp2** | image/jp2 | -
**.jpf** | image/jpx | -
**.jpm** | image/jpm | video/jpm
**.jxs** | image/jxs | -
**.gif** | image/gif | -
**.webp** | image/webp | -
**.exe** | application/vnd.microsoft.portable-executable | -
**n/a** | application/x-elf | -
**n/a** | application/x-object | -
**n/a** | application/x-executable | -
**.so** | application/x-sharedlib | -
**n/a** | application/x-coredump | -
**.a** | application/x-archive | application/x-unix-archive
**.deb** | application/vnd.debian.binary-package | -
**.tar** | application/x-tar | -
**.xar** | application/x-xar | -
**.bz2** | application/x-bzip2 | -
**.fits** | application/fits | image/fits
**.tiff** | image/tiff | -
**.bmp** | image/bmp | image/x-bmp, image/x-ms-bmp
**.123** | application/vnd.lotus-1-2-3 | -
**.ico** | image/x-icon | -
**.mp3** | audio/mpeg | audio/x-mpeg, audio/mp3
**.flac** | audio/flac | -
**.midi** | audio/midi | audio/mid, audio/sp-midi, audio/x-mid, audio/x-midi
**.ape** | audio/ape | -
**.mpc** | audio/musepack | -
**.amr** | audio/amr | audio/amr-nb
**.wav** | audio/wav | audio/x-wav, audio/vnd.wave, audio/wave
**.aiff** | audio/aiff | audio/x-aiff
**.au** | audio/basic | -
**.mpeg** | video/mpeg | -
**.mov** | video/quicktime | -
**.mp4** | video/mp4 | -
**.avif** | image/avif | -
**.3gp** | video/3gpp | video/3gp, audio/3gpp
**.3g2** | video/3gpp2 | video/3g2, audio/3gpp2
**.mp4** | audio/mp4 | audio/x-mp4a
**.mqv** | video/quicktime | -
**.m4a** | audio/x-m4a | -
**.m4v** | video/x-m4v | -
**.heic** | image/heic | -
**.heic** | image/heic-sequence | -
**.heif** | image/heif | -
**.heif** | image/heif-sequence | -
**.mj2** | video/mj2 | -
**.dvb** | video/vnd.dvb.file | -
**.webm** | video/webm | audio/webm
**.avi** | video/x-msvideo | video/avi, video/msvideo
**.flv** | video/x-flv | -
**.mkv** | video/x-matroska | -
**.asf** | video/x-ms-asf | video/asf, video/x-ms-wmv
**.aac** | audio/aac | -
**.voc** | audio/x-unknown | -
**.m3u** | application/vnd.apple.mpegurl | audio/mpegurl
**.rmvb** | application/vnd.rn-realmedia-vbr | -
**.gz** | application/gzip | application/x-gzip, application/x-gunzip, application/gzipped, application/gzip-compressed, application/x-gzip-compressed, gzip/document
**.class** | application/x-java-applet | -
**.swf** | application/x-shockwave-flash | -
**.crx** | application/x-chrome-extension | -
**.ttf** | font/ttf | font/sfnt, application/x-font-ttf, application/font-sfnt
**.woff** | font/woff | -
**.woff2** | font/woff2 | -
**.otf** | font/otf | -
**.ttc** | font/collection | -
**.eot** | application/vnd.ms-fontobject | -
**.wasm** | application/wasm | -
**.shx** | application/vnd.shx | -
**.shp** | application/vnd.shp | -
**.dbf** | application/x-dbf | -
**.dcm** | application/dicom | -
**.rar** | application/x-rar-compressed | application/x-rar
**.djvu** | image/vnd.djvu | -
**.mobi** | application/x-mobipocket-ebook | -
**.lit** | application/x-ms-reader | -
**.bpg** | image/bpg | -
**.cbor** | application/cbor | -
**.sqlite** | application/vnd.sqlite3 | application/x-sqlite3
**.dwg** | image/vnd.dwg | image/x-dwg, application/acad, application/x-acad, application/autocad_dwg, application/dwg, application/x-dwg, application/x-autocad, drawing/dwg
**.nes** | application/vnd.nintendo.snes.rom | -
**.lnk** | application/x-ms-shortcut | -
**.macho** | application/x-mach-binary | -
**.qcp** | audio/qcelp | -
**.icns** | image/x-icns | -
**.hdr** | image/vnd.radiance | -
**.mrc** | application/marc | -
**.mdb** | application/x-msaccess | -
**.accdb** | application/x-msaccess | -
**.zst** | application/zstd | -
**.cab** | application/vnd.ms-cab-compressed | -
**.rpm** | application/x-rpm | -
**.xz** | application/x-xz | -
**.lz** | application/lzip | application/x-lzip
**.torrent** | application/x-bittorrent | -
**.cpio** | application/x-cpio | -
**n/a** | application/tzif | -
**.xcf** | image/x-xcf | -
**.pat** | image/x-gimp-pat | -
**.gbr** | image/x-gimp-gbr | -
**.glb** | model/gltf-binary | -
**.cab** | application/x-installshield | -
**.jxr** | image/jxr | image/vnd.ms-photo
**.parquet** | application/vnd.apache.parquet | application/x-parquet
**.one** | application/onenote | -
**.chm** | application/vnd.ms-htmlhelp | -
**.txt** | text/plain | -
**.svg** | image/svg+xml | -
**.html** | text/html | -
**.xml** | text/xml | application/xml
**.rss** | application/rss+xml | text/rss
**.atom** | application/atom+xml | -
**.x3d** | model/x3d+xml | -
**.kml** | application/vnd.google-earth.kml+xml | -
**.xlf** | application/x-xliff+xml | -
**.dae** | model/vnd.collada+xml | -
**.gml** | application/gml+xml | -
**.gpx** | application/gpx+xml | -
**.tcx** | application/vnd.garmin.tcx+xml | -
**.amf** | application/x-amf | -
**.3mf** | application/vnd.ms-package.3dmanufacturing-3dmodel+xml | -
**.xfdf** | application/vnd.adobe.xfdf | -
**.owl** | application/owl+xml | -
**.html** | application/xhtml+xml | -
**.php** | text/x-php | -
**.js** | text/javascript | application/x-javascript, application/javascript
**.lua** | text/x-lua | -
**.pl** | text/x-perl | -
**.py** | text/x-python | text/x-script.python, application/x-python
**.rb** | text/x-ruby | application/x-ruby
**.json** | application/json | -
**.geojson** | application/geo+json | -
**.har** | application/json | -
**.gltf** | model/gltf+json | -
**.ndjson** | application/x-ndjson | -
**.rtf** | text/rtf | application/rtf
**.srt** | application/x-subrip | application/x-srt, text/x-srt
**.tcl** | text/x-tcl | application/x-tcl
**.csv** | text/csv | -
**.tsv** | text/tab-separated-values | -
**.vcf** | text/vcard | -
**.ics** | text/calendar | -
**.warc** | application/warc | -
**.vtt** | text/vtt | -
**.sh** | text/x-shellscript | text/x-sh, application/x-shellscript, application/x-sh
**.pbm** | image/x-portable-bitmap | -
**.pgm** | image/x-portable-graymap | -
**.ppm** | image/x-portable-pixmap | -
**.pam** | image/x-portable-arbitrarymap | -
Extension | MIME type <br> Aliases | Hierarchy
--------- | ---------------------- | ---------
**n/a** | **application/octet-stream** | root
**.xpm** | **image/x-xpixmap** | xpm>root
**.7z** | **application/x-7z-compressed** | 7z>root
**.zip** | **application/zip** <br> application/x-zip, application/x-zip-compressed | zip>root
**.docx** | **application/vnd.openxmlformats-officedocument.wordprocessingml.document** | docx>zip>root
**.pptx** | **application/vnd.openxmlformats-officedocument.presentationml.presentation** | pptx>zip>root
**.xlsx** | **application/vnd.openxmlformats-officedocument.spreadsheetml.sheet** | xlsx>zip>root
**.epub** | **application/epub+zip** | epub>zip>root
**.apk** | **application/vnd.android.package-archive** | apk>zip>root
**.jar** | **application/java-archive** <br> application/jar, application/jar-archive, application/x-java-archive | jar>zip>root
**.odt** | **application/vnd.oasis.opendocument.text** <br> application/x-vnd.oasis.opendocument.text | odt>zip>root
**.ott** | **application/vnd.oasis.opendocument.text-template** <br> application/x-vnd.oasis.opendocument.text-template | ott>odt>zip>root
**.ods** | **application/vnd.oasis.opendocument.spreadsheet** <br> application/x-vnd.oasis.opendocument.spreadsheet | ods>zip>root
**.ots** | **application/vnd.oasis.opendocument.spreadsheet-template** <br> application/x-vnd.oasis.opendocument.spreadsheet-template | ots>ods>zip>root
**.odp** | **application/vnd.oasis.opendocument.presentation** <br> application/x-vnd.oasis.opendocument.presentation | odp>zip>root
**.otp** | **application/vnd.oasis.opendocument.presentation-template** <br> application/x-vnd.oasis.opendocument.presentation-template | otp>odp>zip>root
**.odg** | **application/vnd.oasis.opendocument.graphics** <br> application/x-vnd.oasis.opendocument.graphics | odg>zip>root
**.otg** | **application/vnd.oasis.opendocument.graphics-template** <br> application/x-vnd.oasis.opendocument.graphics-template | otg>odg>zip>root
**.odf** | **application/vnd.oasis.opendocument.formula** <br> application/x-vnd.oasis.opendocument.formula | odf>zip>root
**.odc** | **application/vnd.oasis.opendocument.chart** <br> application/x-vnd.oasis.opendocument.chart | odc>zip>root
**.sxc** | **application/vnd.sun.xml.calc** | sxc>zip>root
**.kmz** | **application/vnd.google-earth.kmz** | kmz>zip>root
**.vsdx** | **application/vnd.ms-visio.drawing.main+xml** | vsdx>zip>root
**.pdf** | **application/pdf** <br> application/x-pdf | pdf>root
**.fdf** | **application/vnd.fdf** | fdf>root
**n/a** | **application/x-ole-storage** | x-ole-storage>root
**.msi** | **application/x-ms-installer** <br> application/x-windows-installer, application/x-msi | msi>x-ole-storage>root
**.msg** | **application/vnd.ms-outlook** | msg>x-ole-storage>root
**.xls** | **application/vnd.ms-excel** <br> application/msexcel | xls>x-ole-storage>root
**.pub** | **application/vnd.ms-publisher** | pub>x-ole-storage>root
**.ppt** | **application/vnd.ms-powerpoint** <br> application/mspowerpoint | ppt>x-ole-storage>root
**.doc** | **application/msword** <br> application/vnd.ms-word | doc>x-ole-storage>root
**.ps** | **application/postscript** | ps>root
**.psd** | **image/vnd.adobe.photoshop** <br> image/x-psd, application/photoshop | psd>root
**.p7s** | **application/pkcs7-signature** | p7s>root
**.ogg** | **application/ogg** <br> application/x-ogg | ogg>root
**.oga** | **audio/ogg** | oga>ogg>root
**.ogv** | **video/ogg** | ogv>ogg>root
**.png** | **image/png** | png>root
**.png** | **image/vnd.mozilla.apng** | png>png>root
**.jpg** | **image/jpeg** | jpg>root
**.jxl** | **image/jxl** | jxl>root
**.jp2** | **image/jp2** | jp2>root
**.jpf** | **image/jpx** | jpf>root
**.jpm** | **image/jpm** <br> video/jpm | jpm>root
**.jxs** | **image/jxs** | jxs>root
**.gif** | **image/gif** | gif>root
**.webp** | **image/webp** | webp>root
**.exe** | **application/vnd.microsoft.portable-executable** | exe>root
**n/a** | **application/x-elf** | x-elf>root
**n/a** | **application/x-object** | x-object>x-elf>root
**n/a** | **application/x-executable** | x-executable>x-elf>root
**.so** | **application/x-sharedlib** | so>x-elf>root
**n/a** | **application/x-coredump** | x-coredump>x-elf>root
**.a** | **application/x-archive** <br> application/x-unix-archive | a>root
**.deb** | **application/vnd.debian.binary-package** | deb>a>root
**.tar** | **application/x-tar** | tar>root
**.xar** | **application/x-xar** | xar>root
**.bz2** | **application/x-bzip2** | bz2>root
**.fits** | **application/fits** <br> image/fits | fits>root
**.tiff** | **image/tiff** | tiff>root
**.bmp** | **image/bmp** <br> image/x-bmp, image/x-ms-bmp | bmp>root
**.123** | **application/vnd.lotus-1-2-3** | 123>root
**.ico** | **image/x-icon** | ico>root
**.mp3** | **audio/mpeg** <br> audio/x-mpeg, audio/mp3 | mp3>root
**.flac** | **audio/flac** | flac>root
**.midi** | **audio/midi** <br> audio/mid, audio/sp-midi, audio/x-mid, audio/x-midi | midi>root
**.ape** | **audio/ape** | ape>root
**.mpc** | **audio/musepack** | mpc>root
**.amr** | **audio/amr** <br> audio/amr-nb | amr>root
**.wav** | **audio/wav** <br> audio/x-wav, audio/vnd.wave, audio/wave | wav>root
**.aiff** | **audio/aiff** <br> audio/x-aiff | aiff>root
**.au** | **audio/basic** | au>root
**.mpeg** | **video/mpeg** | mpeg>root
**.mov** | **video/quicktime** | mov>root
**.mp4** | **video/mp4** | mp4>root
**.avif** | **image/avif** | avif>mp4>root
**.3gp** | **video/3gpp** <br> video/3gp, audio/3gpp | 3gp>mp4>root
**.3g2** | **video/3gpp2** <br> video/3g2, audio/3gpp2 | 3g2>mp4>root
**.mp4** | **audio/mp4** <br> audio/x-mp4a | mp4>mp4>root
**.mqv** | **video/quicktime** | mqv>mp4>root
**.m4a** | **audio/x-m4a** | m4a>mp4>root
**.m4v** | **video/x-m4v** | m4v>mp4>root
**.heic** | **image/heic** | heic>mp4>root
**.heic** | **image/heic-sequence** | heic>mp4>root
**.heif** | **image/heif** | heif>mp4>root
**.heif** | **image/heif-sequence** | heif>mp4>root
**.mj2** | **video/mj2** | mj2>mp4>root
**.dvb** | **video/vnd.dvb.file** | dvb>mp4>root
**.webm** | **video/webm** <br> audio/webm | webm>root
**.avi** | **video/x-msvideo** <br> video/avi, video/msvideo | avi>root
**.flv** | **video/x-flv** | flv>root
**.mkv** | **video/x-matroska** | mkv>root
**.asf** | **video/x-ms-asf** <br> video/asf, video/x-ms-wmv | asf>root
**.aac** | **audio/aac** | aac>root
**.voc** | **audio/x-unknown** | voc>root
**.m3u** | **application/vnd.apple.mpegurl** <br> audio/mpegurl | m3u>root
**.rmvb** | **application/vnd.rn-realmedia-vbr** | rmvb>root
**.gz** | **application/gzip** <br> application/x-gzip, application/x-gunzip, application/gzipped, application/gzip-compressed, application/x-gzip-compressed, gzip/document | gz>root
**.class** | **application/x-java-applet** | class>root
**.swf** | **application/x-shockwave-flash** | swf>root
**.crx** | **application/x-chrome-extension** | crx>root
**.ttf** | **font/ttf** <br> font/sfnt, application/x-font-ttf, application/font-sfnt | ttf>root
**.woff** | **font/woff** | woff>root
**.woff2** | **font/woff2** | woff2>root
**.otf** | **font/otf** | otf>root
**.ttc** | **font/collection** | ttc>root
**.eot** | **application/vnd.ms-fontobject** | eot>root
**.wasm** | **application/wasm** | wasm>root
**.shx** | **application/vnd.shx** | shx>root
**.shp** | **application/vnd.shp** | shp>shx>root
**.dbf** | **application/x-dbf** | dbf>root
**.dcm** | **application/dicom** | dcm>root
**.rar** | **application/x-rar-compressed** <br> application/x-rar | rar>root
**.djvu** | **image/vnd.djvu** | djvu>root
**.mobi** | **application/x-mobipocket-ebook** | mobi>root
**.lit** | **application/x-ms-reader** | lit>root
**.bpg** | **image/bpg** | bpg>root
**.cbor** | **application/cbor** | cbor>root
**.sqlite** | **application/vnd.sqlite3** <br> application/x-sqlite3 | sqlite>root
**.dwg** | **image/vnd.dwg** <br> image/x-dwg, application/acad, application/x-acad, application/autocad_dwg, application/dwg, application/x-dwg, application/x-autocad, drawing/dwg | dwg>root
**.nes** | **application/vnd.nintendo.snes.rom** | nes>root
**.lnk** | **application/x-ms-shortcut** | lnk>root
**.macho** | **application/x-mach-binary** | macho>root
**.qcp** | **audio/qcelp** | qcp>root
**.icns** | **image/x-icns** | icns>root
**.hdr** | **image/vnd.radiance** | hdr>root
**.mrc** | **application/marc** | mrc>root
**.mdb** | **application/x-msaccess** | mdb>root
**.accdb** | **application/x-msaccess** | accdb>root
**.zst** | **application/zstd** | zst>root
**.cab** | **application/vnd.ms-cab-compressed** | cab>root
**.rpm** | **application/x-rpm** | rpm>root
**.xz** | **application/x-xz** | xz>root
**.lz** | **application/lzip** <br> application/x-lzip | lz>root
**.torrent** | **application/x-bittorrent** | torrent>root
**.cpio** | **application/x-cpio** | cpio>root
**n/a** | **application/tzif** | tzif>root
**.xcf** | **image/x-xcf** | xcf>root
**.pat** | **image/x-gimp-pat** | pat>root
**.gbr** | **image/x-gimp-gbr** | gbr>root
**.glb** | **model/gltf-binary** | glb>root
**.cab** | **application/x-installshield** | cab>root
**.jxr** | **image/jxr** <br> image/vnd.ms-photo | jxr>root
**.parquet** | **application/vnd.apache.parquet** <br> application/x-parquet | parquet>root
**.one** | **application/onenote** | one>root
**.chm** | **application/vnd.ms-htmlhelp** | chm>root
**.wpd** | **application/vnd.wordperfect** | wpd>root
**.dxf** | **image/vnd.dxf** | dxf>root
**.txt** | **text/plain** | txt>root
**.svg** | **image/svg+xml** | svg>txt>root
**.html** | **text/html** | html>txt>root
**.xml** | **text/xml** <br> application/xml | xml>txt>root
**.rss** | **application/rss+xml** <br> text/rss | rss>xml>txt>root
**.atom** | **application/atom+xml** | atom>xml>txt>root
**.x3d** | **model/x3d+xml** | x3d>xml>txt>root
**.kml** | **application/vnd.google-earth.kml+xml** | kml>xml>txt>root
**.xlf** | **application/x-xliff+xml** | xlf>xml>txt>root
**.dae** | **model/vnd.collada+xml** | dae>xml>txt>root
**.gml** | **application/gml+xml** | gml>xml>txt>root
**.gpx** | **application/gpx+xml** | gpx>xml>txt>root
**.tcx** | **application/vnd.garmin.tcx+xml** | tcx>xml>txt>root
**.amf** | **application/x-amf** | amf>xml>txt>root
**.3mf** | **application/vnd.ms-package.3dmanufacturing-3dmodel+xml** | 3mf>xml>txt>root
**.xfdf** | **application/vnd.adobe.xfdf** | xfdf>xml>txt>root
**.owl** | **application/owl+xml** | owl>xml>txt>root
**.html** | **application/xhtml+xml** | html>xml>txt>root
**.php** | **text/x-php** | php>txt>root
**.js** | **text/javascript** <br> application/x-javascript, application/javascript | js>txt>root
**.lua** | **text/x-lua** | lua>txt>root
**.pl** | **text/x-perl** | pl>txt>root
**.py** | **text/x-python** <br> text/x-script.python, application/x-python | py>txt>root
**.rb** | **text/x-ruby** <br> application/x-ruby | rb>txt>root
**.json** | **application/json** | json>txt>root
**.geojson** | **application/geo+json** | geojson>json>txt>root
**.har** | **application/json** | har>json>txt>root
**.gltf** | **model/gltf+json** | gltf>json>txt>root
**.ndjson** | **application/x-ndjson** | ndjson>txt>root
**.rtf** | **text/rtf** <br> application/rtf | rtf>txt>root
**.srt** | **application/x-subrip** <br> application/x-srt, text/x-srt | srt>txt>root
**.tcl** | **text/x-tcl** <br> application/x-tcl | tcl>txt>root
**.csv** | **text/csv** | csv>txt>root
**.tsv** | **text/tab-separated-values** | tsv>txt>root
**.vcf** | **text/vcard** | vcf>txt>root
**.ics** | **text/calendar** | ics>txt>root
**.warc** | **application/warc** | warc>txt>root
**.vtt** | **text/vtt** | vtt>txt>root
**.sh** | **text/x-shellscript** <br> text/x-sh, application/x-shellscript, application/x-sh | sh>txt>root
**.pbm** | **image/x-portable-bitmap** | pbm>txt>root
**.pgm** | **image/x-portable-graymap** | pgm>txt>root
**.ppm** | **image/x-portable-pixmap** | ppm>txt>root
**.pam** | **image/x-portable-arbitrarymap** | pam>txt>root

View file

@ -24,7 +24,7 @@ var root = newMIME("application/octet-stream", "",
woff2, otf, ttc, eot, wasm, shx, dbf, dcm, rar, djvu, mobi, lit, bpg, cbor,
sqlite3, dwg, nes, lnk, macho, qcp, icns, hdr, mrc, mdb, accdb, zstd, cab,
rpm, xz, lzip, torrent, cpio, tzif, xcf, pat, gbr, glb, cabIS, jxr, parquet,
oneNote, chm,
oneNote, chm, wpd, dxf,
// Keep text last because it is the slowest check.
text,
)
@ -65,10 +65,9 @@ var (
jar = newMIME("application/java-archive", ".jar", magic.Jar).
alias("application/jar", "application/jar-archive", "application/x-java-archive")
apk = newMIME("application/vnd.android.package-archive", ".apk", magic.APK)
ole = newMIME("application/x-ole-storage", "", magic.Ole, msi, aaf, msg, xls, pub, ppt, doc)
ole = newMIME("application/x-ole-storage", "", magic.Ole, msi, msg, xls, pub, ppt, doc)
msi = newMIME("application/x-ms-installer", ".msi", magic.Msi).
alias("application/x-windows-installer", "application/x-msi")
aaf = newMIME("application/octet-stream", ".aaf", magic.Aaf)
doc = newMIME("application/msword", ".doc", magic.Doc).
alias("application/vnd.ms-word")
ppt = newMIME("application/vnd.ms-powerpoint", ".ppt", magic.Ppt).
@ -286,4 +285,6 @@ var (
cbor = newMIME("application/cbor", ".cbor", magic.CBOR)
oneNote = newMIME("application/onenote", ".one", magic.One)
chm = newMIME("application/vnd.ms-htmlhelp", ".chm", magic.CHM)
wpd = newMIME("application/vnd.wordperfect", ".wpd", magic.WPD)
dxf = newMIME("image/vnd.dxf", ".dxf", magic.DXF)
)

View file

@ -1,5 +1,63 @@
# Changelog
## 0.36.2
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.2.
### Bug Fixes
- Fix context propagation for logs to ensure logger instances correctly inherit span and hub information from their creation context ([#1118](https://github.com/getsentry/sentry-go/pull/1118))
- Logs now properly propagate trace context from the logger's original context, even when emitted in a different context
- The logger will first check the emission context, then fall back to its creation context, and finally to the current hub
## 0.36.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.1.
### Bug Fixes
- Prevent panic when converting error chains containing non-comparable error types by using a safe fallback for visited detection in exception conversion ([#1113](https://github.com/getsentry/sentry-go/pull/1113))
## 0.36.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.0.
### Breaking Changes
- Behavioral change for the `MaxBreadcrumbs` client option. Removed the hard limit of 100 breadcrumbs, allowing users to set a larger limit and also changed the default limit from 30 to 100 ([#1106](https://github.com/getsentry/sentry-go/pull/1106)))
- The changes to error handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075)) will affect issue grouping. It is expected that any wrapped and complex errors will be grouped under a new issue group.
### Features
- Add support for improved issue grouping with enhanced error chain handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075))
The SDK now provides better handling of complex error scenarios, particularly when dealing with multiple related errors or error chains. This feature automatically detects and properly structures errors created with Go's `errors.Join()` function and other multi-error patterns.
```go
// Multiple errors are now properly grouped and displayed in Sentry
err1 := errors.New("err1")
err2 := errors.New("err2")
combinedErr := errors.Join(err1, err2)
// When captured, these will be shown as related exceptions in Sentry
sentry.CaptureException(combinedErr)
```
- Add `TraceIgnoreStatusCodes` option to allow filtering of HTTP transactions based on status codes ([#1089](https://github.com/getsentry/sentry-go/pull/1089))
- Configure which HTTP status codes should not be traced by providing single codes or ranges
- Example: `TraceIgnoreStatusCodes: [][]int{{404}, {500, 599}}` ignores 404 and server errors 500-599
### Bug Fixes
- Fix logs being incorrectly filtered by `BeforeSend` callback ([#1109](https://github.com/getsentry/sentry-go/pull/1109))
- Logs now bypass the `processEvent` method and are sent directly to the transport
- This ensures logs are only filtered by `BeforeSendLog`, not by the error/message `BeforeSend` callback
### Misc
- Add support for Go 1.25 and drop support for Go 1.22 ([#1103](https://github.com/getsentry/sentry-go/pull/1103))
## 0.35.3
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.3.

View file

@ -70,7 +70,7 @@ mod-tidy: ## Check go.mod tidiness
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
(cd "$${dir}" && go mod tidy -go=1.22 -compat=1.22); \
(cd "$${dir}" && go mod tidy -go=1.23 -compat=1.23); \
done; \
git diff --exit-code;
.PHONY: mod-tidy

View file

@ -125,7 +125,8 @@ func (l *BatchLogger) run(ctx context.Context) {
func (l *BatchLogger) processEvent(logs []Log) {
event := NewEvent()
event.Timestamp = time.Now()
event.EventID = EventID(uuid())
event.Type = logEvent.Type
event.Logs = logs
l.client.CaptureEvent(event, nil, nil)
l.client.Transport.SendEvent(event)
}

View file

@ -5,7 +5,6 @@ import (
"crypto/x509"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
@ -15,24 +14,31 @@ import (
"time"
"github.com/getsentry/sentry-go/internal/debug"
"github.com/getsentry/sentry-go/internal/debuglog"
)
// The identifier of the SDK.
const sdkIdentifier = "sentry.go"
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
const maxErrorDepth = 10
const (
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
maxErrorDepth = 100
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
// would be rejected by Sentry.
const defaultMaxSpans = 1000
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
// would be rejected by Sentry.
defaultMaxSpans = 1000
// defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to
// an event. Can be overwritten with the MaxBreadcrumbs option.
defaultMaxBreadcrumbs = 100
)
// hostname is the host name reported by the kernel. It is precomputed once to
// avoid syscalls when capturing events.
@ -78,8 +84,8 @@ type usageError struct {
}
// DebugLogger is an instance of log.Logger that is used to provide debug information about running Sentry Client
// can be enabled by either using DebugLogger.SetOutput directly or with Debug client option.
var DebugLogger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
// can be enabled by either using debuglog.SetOutput directly or with Debug client option.
var DebugLogger = debuglog.GetLogger()
// EventProcessor is a function that processes an event.
// Event processors are used to change an event before it is sent to Sentry.
@ -228,6 +234,21 @@ type ClientOptions struct {
Tags map[string]string
// EnableLogs controls when logs should be emitted.
EnableLogs bool
// TraceIgnoreStatusCodes is a list of HTTP status codes that should not be traced.
// Each element can be either:
// - A single-element slice [code] for a specific status code
// - A two-element slice [min, max] for a range of status codes (inclusive)
// When an HTTP request results in a status code that matches any of these codes or ranges,
// the transaction will not be sent to Sentry.
//
// Examples:
// [][]int{{404}} // ignore only status code 404
// [][]int{{400, 405}} // ignore status codes 400-405
// [][]int{{404}, {500}} // ignore status codes 404 and 500
// [][]int{{404}, {400, 405}, {500, 599}} // ignore 404, range 400-405, and range 500-599
//
// By default, this is empty and all status codes are traced.
TraceIgnoreStatusCodes [][]int
}
// Client is the underlying processor that is used by the main API and Hub
@ -281,7 +302,7 @@ func NewClient(options ClientOptions) (*Client, error) {
if debugWriter == nil {
debugWriter = os.Stderr
}
DebugLogger.SetOutput(debugWriter)
debuglog.SetOutput(debugWriter)
}
if options.Dsn == "" {
@ -386,12 +407,12 @@ func (client *Client) setupIntegrations() {
for _, integration := range integrations {
if client.integrationAlreadyInstalled(integration.Name()) {
DebugLogger.Printf("Integration %s is already installed\n", integration.Name())
debuglog.Printf("Integration %s is already installed\n", integration.Name())
continue
}
client.integrations = append(client.integrations, integration)
integration.SetupOnce(client)
DebugLogger.Printf("Integration installed: %s\n", integration.Name())
debuglog.Printf("Integration installed: %s\n", integration.Name())
}
sort.Slice(client.integrations, func(i, j int) bool {
@ -632,7 +653,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
// options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
DebugLogger.Println("Event dropped due to SampleRate hit.")
debuglog.Println("Event dropped due to SampleRate hit.")
return nil
}
@ -648,7 +669,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
case transactionType:
if client.options.BeforeSendTransaction != nil {
if event = client.options.BeforeSendTransaction(event, hint); event == nil {
DebugLogger.Println("Transaction dropped due to BeforeSendTransaction callback.")
debuglog.Println("Transaction dropped due to BeforeSendTransaction callback.")
return nil
}
}
@ -656,7 +677,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
default:
if client.options.BeforeSend != nil {
if event = client.options.BeforeSend(event, hint); event == nil {
DebugLogger.Println("Event dropped due to BeforeSend callback.")
debuglog.Println("Event dropped due to BeforeSend callback.")
return nil
}
}
@ -723,7 +744,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
id := event.EventID
event = processor(event, hint)
if event == nil {
DebugLogger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
debuglog.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
return nil
}
}
@ -732,7 +753,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
id := event.EventID
event = processor(event, hint)
if event == nil {
DebugLogger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
debuglog.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
return nil
}
}

View file

@ -1,233 +1,37 @@
package sentry
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/getsentry/sentry-go/internal/protocol"
)
type scheme string
// Re-export protocol types to maintain public API compatibility
const (
schemeHTTP scheme = "http"
schemeHTTPS scheme = "https"
)
func (scheme scheme) defaultPort() int {
switch scheme {
case schemeHTTPS:
return 443
case schemeHTTP:
return 80
default:
return 80
}
// Dsn is used as the remote address source to client transport.
type Dsn struct {
protocol.Dsn
}
// DsnParseError represents an error that occurs if a Sentry
// DSN cannot be parsed.
type DsnParseError struct {
Message string
}
func (e DsnParseError) Error() string {
return "[Sentry] DsnParseError: " + e.Message
}
// Dsn is used as the remote address source to client transport.
type Dsn struct {
scheme scheme
publicKey string
secretKey string
host string
port int
path string
projectID string
}
type DsnParseError = protocol.DsnParseError
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
// function directly. It is provided for use in custom Transport
// implementations.
func NewDsn(rawURL string) (*Dsn, error) {
// Parse
parsedURL, err := url.Parse(rawURL)
protocolDsn, err := protocol.NewDsn(rawURL)
if err != nil {
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
return nil, err
}
// Scheme
var scheme scheme
switch parsedURL.Scheme {
case "http":
scheme = schemeHTTP
case "https":
scheme = schemeHTTPS
default:
return nil, &DsnParseError{"invalid scheme"}
}
// PublicKey
publicKey := parsedURL.User.Username()
if publicKey == "" {
return nil, &DsnParseError{"empty username"}
}
// SecretKey
var secretKey string
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
secretKey = parsedSecretKey
}
// Host
host := parsedURL.Hostname()
if host == "" {
return nil, &DsnParseError{"empty host"}
}
// Port
var port int
if p := parsedURL.Port(); p != "" {
port, err = strconv.Atoi(p)
if err != nil {
return nil, &DsnParseError{"invalid port"}
}
} else {
port = scheme.defaultPort()
}
// ProjectID
if parsedURL.Path == "" || parsedURL.Path == "/" {
return nil, &DsnParseError{"empty project id"}
}
pathSegments := strings.Split(parsedURL.Path[1:], "/")
projectID := pathSegments[len(pathSegments)-1]
if projectID == "" {
return nil, &DsnParseError{"empty project id"}
}
// Path
var path string
if len(pathSegments) > 1 {
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
}
return &Dsn{
scheme: scheme,
publicKey: publicKey,
secretKey: secretKey,
host: host,
port: port,
path: path,
projectID: projectID,
}, nil
return &Dsn{Dsn: *protocolDsn}, nil
}
// String formats Dsn struct into a valid string url.
func (dsn Dsn) String() string {
var url string
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
if dsn.secretKey != "" {
url += fmt.Sprintf(":%s", dsn.secretKey)
}
url += fmt.Sprintf("@%s", dsn.host)
if dsn.port != dsn.scheme.defaultPort() {
url += fmt.Sprintf(":%d", dsn.port)
}
if dsn.path != "" {
url += dsn.path
}
url += fmt.Sprintf("/%s", dsn.projectID)
return url
}
// Get the scheme of the DSN.
func (dsn Dsn) GetScheme() string {
return string(dsn.scheme)
}
// Get the public key of the DSN.
func (dsn Dsn) GetPublicKey() string {
return dsn.publicKey
}
// Get the secret key of the DSN.
func (dsn Dsn) GetSecretKey() string {
return dsn.secretKey
}
// Get the host of the DSN.
func (dsn Dsn) GetHost() string {
return dsn.host
}
// Get the port of the DSN.
func (dsn Dsn) GetPort() int {
return dsn.port
}
// Get the path of the DSN.
func (dsn Dsn) GetPath() string {
return dsn.path
}
// Get the project ID of the DSN.
func (dsn Dsn) GetProjectID() string {
return dsn.projectID
}
// GetAPIURL returns the URL of the envelope endpoint of the project
// associated with the DSN.
func (dsn Dsn) GetAPIURL() *url.URL {
var rawURL string
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
if dsn.port != dsn.scheme.defaultPort() {
rawURL += fmt.Sprintf(":%d", dsn.port)
}
if dsn.path != "" {
rawURL += dsn.path
}
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope")
parsedURL, _ := url.Parse(rawURL)
return parsedURL
}
// RequestHeaders returns all the necessary headers that have to be used in the transport when seinding events
// RequestHeaders returns all the necessary headers that have to be used in the transport when sending events
// to the /store endpoint.
//
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
// against the /envelope endpoint are added automatically.
func (dsn Dsn) RequestHeaders() map[string]string {
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), SDKVersion, dsn.publicKey)
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
}
return map[string]string{
"Content-Type": "application/json",
"X-Sentry-Auth": auth,
}
}
// MarshalJSON converts the Dsn struct to JSON.
func (dsn Dsn) MarshalJSON() ([]byte, error) {
return json.Marshal(dsn.String())
}
// UnmarshalJSON converts JSON data to the Dsn struct.
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
var str string
_ = json.Unmarshal(data, &str)
newDsn, err := NewDsn(str)
if err != nil {
return err
}
*dsn = *newDsn
return nil
return dsn.Dsn.RequestHeaders(SDKVersion)
}

View file

@ -60,7 +60,7 @@ func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
}
if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
if publicKey := dsn.GetPublicKey(); publicKey != "" {
entries["public_key"] = publicKey
}
}
@ -136,7 +136,7 @@ func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSampli
}
if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
if publicKey := dsn.GetPublicKey(); publicKey != "" {
entries["public_key"] = publicKey
}
}

132
vendor/github.com/getsentry/sentry-go/exception.go generated vendored Normal file
View file

@ -0,0 +1,132 @@
package sentry
import (
"fmt"
"reflect"
"slices"
)
const (
MechanismTypeGeneric string = "generic"
MechanismTypeChained string = "chained"
MechanismTypeUnwrap string = "unwrap"
MechanismSourceCause string = "cause"
)
type visited struct {
comparable map[error]struct{}
msgs map[string]struct{}
}
func (v *visited) seenError(err error) bool {
t := reflect.TypeOf(err)
if t == nil {
return false
}
if t.Comparable() {
if _, ok := v.comparable[err]; ok {
return true
}
v.comparable[err] = struct{}{}
return false
}
key := t.String() + err.Error()
if _, ok := v.msgs[key]; ok {
return true
}
v.msgs[key] = struct{}{}
return false
}
func convertErrorToExceptions(err error, maxErrorDepth int) []Exception {
var exceptions []Exception
vis := &visited{
make(map[error]struct{}),
make(map[string]struct{}),
}
convertErrorDFS(err, &exceptions, nil, "", vis, maxErrorDepth, 0)
// mechanism type is used for debugging purposes, but since we can't really distinguish the origin of who invoked
// captureException, we set it to nil if the error is not chained.
if len(exceptions) == 1 {
exceptions[0].Mechanism = nil
}
slices.Reverse(exceptions)
// Add a trace of the current stack to the top level(outermost) error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if len(exceptions) > 0 && exceptions[len(exceptions)-1].Stacktrace == nil {
exceptions[len(exceptions)-1].Stacktrace = NewStacktrace()
}
return exceptions
}
func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited *visited, maxErrorDepth int, currentDepth int) {
if err == nil {
return
}
if visited.seenError(err) {
return
}
_, isExceptionGroup := err.(interface{ Unwrap() []error })
exception := Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
}
currentID := len(*exceptions)
var mechanismType string
if parentID == nil {
mechanismType = MechanismTypeGeneric
source = ""
} else {
mechanismType = MechanismTypeChained
}
exception.Mechanism = &Mechanism{
Type: mechanismType,
ExceptionID: currentID,
ParentID: parentID,
Source: source,
IsExceptionGroup: isExceptionGroup,
}
*exceptions = append(*exceptions, exception)
if maxErrorDepth >= 0 && currentDepth >= maxErrorDepth {
return
}
switch v := err.(type) {
case interface{ Unwrap() []error }:
unwrapped := v.Unwrap()
for i := range unwrapped {
if unwrapped[i] != nil {
childSource := fmt.Sprintf("errors[%d]", i)
convertErrorDFS(unwrapped[i], exceptions, &currentID, childSource, visited, maxErrorDepth, currentDepth+1)
}
}
case interface{ Unwrap() error }:
unwrapped := v.Unwrap()
if unwrapped != nil {
convertErrorDFS(unwrapped, exceptions, &currentID, MechanismTypeUnwrap, visited, maxErrorDepth, currentDepth+1)
}
case interface{ Cause() error }:
cause := v.Cause()
if cause != nil {
convertErrorDFS(cause, exceptions, &currentID, MechanismSourceCause, visited, maxErrorDepth, currentDepth+1)
}
}
}

View file

@ -5,6 +5,8 @@ import (
"fmt"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
)
type contextKey int
@ -18,14 +20,6 @@ const (
RequestContextKey = contextKey(2)
)
// defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to
// an event. Can be overwritten with the maxBreadcrumbs option.
const defaultMaxBreadcrumbs = 30
// maxBreadcrumbs is the absolute maximum number of breadcrumbs added to an
// event. The maxBreadcrumbs option cannot be set higher than this value.
const maxBreadcrumbs = 100
// currentHub is the initial Hub with no Client bound and an empty Scope.
var currentHub = NewHub(nil, NewScope())
@ -289,7 +283,7 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
// If there's no client, just store it on the scope straight away
if client == nil {
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
hub.Scope().AddBreadcrumb(breadcrumb, defaultMaxBreadcrumbs)
return
}
@ -299,8 +293,6 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
return
case limit == 0:
limit = defaultMaxBreadcrumbs
case limit > maxBreadcrumbs:
limit = maxBreadcrumbs
}
if client.options.BeforeBreadcrumb != nil {
@ -308,7 +300,7 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
hint = &BreadcrumbHint{}
}
if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
DebugLogger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
debuglog.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
return
}
}

View file

@ -8,6 +8,8 @@ import (
"runtime/debug"
"strings"
"sync"
"github.com/getsentry/sentry-go/internal/debuglog"
)
// ================================
@ -32,7 +34,7 @@ func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
mi.once.Do(func() {
info, ok := debug.ReadBuildInfo()
if !ok {
DebugLogger.Print("The Modules integration is not available in binaries built without module support.")
debuglog.Print("The Modules integration is not available in binaries built without module support.")
return
}
mi.modules = extractModules(info)
@ -141,7 +143,7 @@ func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event
for _, suspect := range suspects {
for _, pattern := range iei.ignoreErrors {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
DebugLogger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
debuglog.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}
@ -203,7 +205,7 @@ func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint)
for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
DebugLogger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
debuglog.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}

View file

@ -3,16 +3,14 @@ package sentry
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"reflect"
"slices"
"strings"
"time"
"github.com/getsentry/sentry-go/attribute"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
@ -41,18 +39,8 @@ const (
)
// SdkInfo contains all metadata about the SDK.
type SdkInfo struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Integrations []string `json:"integrations,omitempty"`
Packages []SdkPackage `json:"packages,omitempty"`
}
// SdkPackage describes a package that was installed.
type SdkPackage struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}
type SdkInfo = protocol.SdkInfo
type SdkPackage = protocol.SdkPackage
// TODO: This type could be more useful, as map of interface{} is too generic
// and requires a lot of type assertions in beforeBreadcrumb calls
@ -249,11 +237,11 @@ var sensitiveHeaders = map[string]struct{}{
// NewRequest avoids operations that depend on network access. In particular, it
// does not read r.Body.
func NewRequest(r *http.Request) *Request {
protocol := schemeHTTP
prot := protocol.SchemeHTTP
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
protocol = schemeHTTPS
prot = protocol.SchemeHTTPS
}
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
url := fmt.Sprintf("%s://%s%s", prot, r.Host, r.URL.Path)
var cookies string
var env map[string]string
@ -296,7 +284,7 @@ func NewRequest(r *http.Request) *Request {
// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
Type string `json:"type,omitempty"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Source string `json:"source,omitempty"`
@ -425,64 +413,84 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
return
}
err := exception
for i := 0; err != nil && (i < maxErrorDepth || maxErrorDepth == -1); i++ {
// Add the current error to the exception slice with its details
e.Exception = append(e.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
// Attempt to unwrap the error using the standard library's Unwrap method.
// If errors.Unwrap returns nil, it means either there is no error to unwrap,
// or the error does not implement the Unwrap method.
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
// The error was successfully unwrapped using the standard library's Unwrap method.
err = unwrappedErr
continue
}
cause, ok := err.(interface{ Cause() error })
if !ok {
// We cannot unwrap the error further.
break
}
// The error implements the Cause method, indicating it may have been wrapped
// using the github.com/pkg/errors package.
err = cause.Cause()
}
// Add a trace of the current stack to the most recent error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if e.Exception[0].Stacktrace == nil {
e.Exception[0].Stacktrace = NewStacktrace()
}
if len(e.Exception) <= 1 {
exceptions := convertErrorToExceptions(exception, maxErrorDepth)
if len(exceptions) == 0 {
return
}
// event.Exception should be sorted such that the most recent error is last.
slices.Reverse(e.Exception)
e.Exception = exceptions
}
for i := range e.Exception {
e.Exception[i].Mechanism = &Mechanism{
IsExceptionGroup: true,
ExceptionID: i,
Type: "generic",
// ToEnvelope converts the Event to a Sentry envelope.
// This includes the event data and any attachments as separate envelope items.
func (e *Event) ToEnvelope(dsn *protocol.Dsn) (*protocol.Envelope, error) {
return e.ToEnvelopeWithTime(dsn, time.Now())
}
// ToEnvelopeWithTime converts the Event to a Sentry envelope with a specific sentAt time.
// This is primarily useful for testing with predictable timestamps.
func (e *Event) ToEnvelopeWithTime(dsn *protocol.Dsn, sentAt time.Time) (*protocol.Envelope, error) {
// Create envelope header with trace context
trace := make(map[string]string)
if dsc := e.sdkMetaData.dsc; dsc.HasEntries() {
for k, v := range dsc.Entries {
trace[k] = v
}
if i == 0 {
continue
}
e.Exception[i].Mechanism.ParentID = Pointer(i - 1)
header := &protocol.EnvelopeHeader{
EventID: string(e.EventID),
SentAt: sentAt,
Trace: trace,
}
if dsn != nil {
header.Dsn = dsn.String()
}
header.Sdk = &e.Sdk
envelope := protocol.NewEnvelope(header)
eventBody, err := json.Marshal(e)
if err != nil {
// Try fallback: remove problematic fields and retry
e.Breadcrumbs = nil
e.Contexts = nil
e.Extra = map[string]interface{}{
"info": fmt.Sprintf("Could not encode original event as JSON. "+
"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
"Please verify the data you attach to the scope. "+
"Error: %s", err),
}
eventBody, err = json.Marshal(e)
if err != nil {
return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err)
}
DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields")
}
var mainItem *protocol.EnvelopeItem
switch e.Type {
case transactionType:
mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody)
case checkInType:
mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody)
case logEvent.Type:
mainItem = protocol.NewLogItem(len(e.Logs), eventBody)
default:
mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody)
}
envelope.AddItem(mainItem)
for _, attachment := range e.Attachments {
attachmentItem := protocol.NewAttachmentItem(attachment.Filename, attachment.ContentType, attachment.Payload)
envelope.AddItem(attachmentItem)
}
return envelope, nil
}
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,

View file

@ -0,0 +1,35 @@
package debuglog
import (
"io"
"log"
)
// logger is the global debug logger instance.
var logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
// SetOutput changes the output destination of the logger.
func SetOutput(w io.Writer) {
logger.SetOutput(w)
}
// GetLogger returns the current logger instance.
// This function is thread-safe and can be called concurrently.
func GetLogger() *log.Logger {
return logger
}
// Printf calls Printf on the underlying logger.
func Printf(format string, args ...interface{}) {
logger.Printf(format, args...)
}
// Println calls Println on the underlying logger.
func Println(args ...interface{}) {
logger.Println(args...)
}
// Print calls Print on the underlying logger.
func Print(args ...interface{}) {
logger.Print(args...)
}

View file

@ -0,0 +1,571 @@
package http
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
const (
apiVersion = 7
defaultTimeout = time.Second * 30
defaultQueueSize = 1000
// maxDrainResponseBytes is the maximum number of bytes that transport
// implementations will read from response bodies when draining them.
maxDrainResponseBytes = 16 << 10
)
var (
ErrTransportQueueFull = errors.New("transport queue full")
ErrTransportClosed = errors.New("transport is closed")
)
type TransportOptions struct {
Dsn string
HTTPClient *http.Client
HTTPTransport http.RoundTripper
HTTPProxy string
HTTPSProxy string
CaCerts *x509.CertPool
}
func getProxyConfig(options TransportOptions) func(*http.Request) (*url.URL, error) {
if len(options.HTTPSProxy) > 0 {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPSProxy)
}
}
if len(options.HTTPProxy) > 0 {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPProxy)
}
}
return http.ProxyFromEnvironment
}
func getTLSConfig(options TransportOptions) *tls.Config {
if options.CaCerts != nil {
return &tls.Config{
RootCAs: options.CaCerts,
MinVersion: tls.VersionTLS12,
}
}
return nil
}
func getSentryRequestFromEnvelope(ctx context.Context, dsn *protocol.Dsn, envelope *protocol.Envelope) (r *http.Request, err error) {
defer func() {
if r != nil {
sdkName := envelope.Header.Sdk.Name
sdkVersion := envelope.Header.Sdk.Version
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", sdkName, sdkVersion))
r.Header.Set("Content-Type", "application/x-sentry-envelope")
auth := fmt.Sprintf("Sentry sentry_version=%d, "+
"sentry_client=%s/%s, sentry_key=%s", apiVersion, sdkName, sdkVersion, dsn.GetPublicKey())
if dsn.GetSecretKey() != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey())
}
r.Header.Set("X-Sentry-Auth", auth)
}
}()
var buf bytes.Buffer
_, err = envelope.WriteTo(&buf)
if err != nil {
return nil, err
}
return http.NewRequestWithContext(
ctx,
http.MethodPost,
dsn.GetAPIURL().String(),
&buf,
)
}
func categoryFromEnvelope(envelope *protocol.Envelope) ratelimit.Category {
if envelope == nil || len(envelope.Items) == 0 {
return ratelimit.CategoryAll
}
for _, item := range envelope.Items {
if item == nil || item.Header == nil {
continue
}
switch item.Header.Type {
case protocol.EnvelopeItemTypeEvent:
return ratelimit.CategoryError
case protocol.EnvelopeItemTypeTransaction:
return ratelimit.CategoryTransaction
case protocol.EnvelopeItemTypeCheckIn:
return ratelimit.CategoryMonitor
case protocol.EnvelopeItemTypeLog:
return ratelimit.CategoryLog
case protocol.EnvelopeItemTypeAttachment:
continue
default:
return ratelimit.CategoryAll
}
}
return ratelimit.CategoryAll
}
// SyncTransport is a blocking implementation of Transport.
//
// Clients using this transport will send requests to Sentry sequentially and
// block until a response is returned.
//
// The blocking behavior is useful in a limited set of use cases. For example,
// use it when deploying code to a Function as a Service ("Serverless")
// platform, where any work happening in a background goroutine is not
// guaranteed to execute.
//
// For most cases, prefer AsyncTransport.
type SyncTransport struct {
dsn *protocol.Dsn
client *http.Client
transport http.RoundTripper
mu sync.Mutex
limits ratelimit.Map
Timeout time.Duration
}
func NewSyncTransport(options TransportOptions) protocol.TelemetryTransport {
dsn, err := protocol.NewDsn(options.Dsn)
if err != nil || dsn == nil {
debuglog.Printf("Transport is disabled: invalid dsn: %v\n", err)
return NewNoopTransport()
}
transport := &SyncTransport{
Timeout: defaultTimeout,
limits: make(ratelimit.Map),
dsn: dsn,
}
if options.HTTPTransport != nil {
transport.transport = options.HTTPTransport
} else {
transport.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
transport.client = options.HTTPClient
} else {
transport.client = &http.Client{
Transport: transport.transport,
Timeout: transport.Timeout,
}
}
return transport
}
func (t *SyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
return t.SendEnvelopeWithContext(context.Background(), envelope)
}
func (t *SyncTransport) Close() {}
func (t *SyncTransport) SendEvent(event protocol.EnvelopeConvertible) {
envelope, err := event.ToEnvelope(t.dsn)
if err != nil {
debuglog.Printf("Failed to convert to envelope: %v", err)
return
}
if envelope == nil {
debuglog.Printf("Error: event with empty envelope")
return
}
if err := t.SendEnvelope(envelope); err != nil {
debuglog.Printf("Error sending the envelope: %v", err)
}
}
func (t *SyncTransport) IsRateLimited(category ratelimit.Category) bool {
return t.disabled(category)
}
func (t *SyncTransport) SendEnvelopeWithContext(ctx context.Context, envelope *protocol.Envelope) error {
if envelope == nil {
debuglog.Printf("Error: provided empty envelope")
return nil
}
category := categoryFromEnvelope(envelope)
if t.disabled(category) {
return nil
}
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
if err != nil {
debuglog.Printf("There was an issue creating the request: %v", err)
return err
}
response, err := t.client.Do(request)
if err != nil {
debuglog.Printf("There was an issue with sending an event: %v", err)
return err
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
debuglog.Printf("Error while reading response body: %v", err)
}
debuglog.Printf("Sending %s failed with the following error: %s", envelope.Header.EventID, string(b))
}
t.mu.Lock()
if t.limits == nil {
t.limits = make(ratelimit.Map)
}
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
return response.Body.Close()
}
func (t *SyncTransport) Flush(_ time.Duration) bool {
return true
}
func (t *SyncTransport) FlushWithContext(_ context.Context) bool {
return true
}
func (t *SyncTransport) disabled(c ratelimit.Category) bool {
t.mu.Lock()
defer t.mu.Unlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
// AsyncTransport is the default, non-blocking, implementation of Transport.
//
// Clients using this transport will enqueue requests in a queue and return to
// the caller before any network communication has happened. Requests are sent
// to Sentry sequentially from a background goroutine.
type AsyncTransport struct {
dsn *protocol.Dsn
client *http.Client
transport http.RoundTripper
queue chan *protocol.Envelope
mu sync.RWMutex
limits ratelimit.Map
done chan struct{}
wg sync.WaitGroup
flushRequest chan chan struct{}
sentCount int64
droppedCount int64
errorCount int64
QueueSize int
Timeout time.Duration
startOnce sync.Once
closeOnce sync.Once
}
func NewAsyncTransport(options TransportOptions) protocol.TelemetryTransport {
dsn, err := protocol.NewDsn(options.Dsn)
if err != nil || dsn == nil {
debuglog.Printf("Transport is disabled: invalid dsn: %v", err)
return NewNoopTransport()
}
transport := &AsyncTransport{
QueueSize: defaultQueueSize,
Timeout: defaultTimeout,
done: make(chan struct{}),
limits: make(ratelimit.Map),
dsn: dsn,
}
transport.queue = make(chan *protocol.Envelope, transport.QueueSize)
transport.flushRequest = make(chan chan struct{})
if options.HTTPTransport != nil {
transport.transport = options.HTTPTransport
} else {
transport.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
transport.client = options.HTTPClient
} else {
transport.client = &http.Client{
Transport: transport.transport,
Timeout: transport.Timeout,
}
}
transport.start()
return transport
}
func (t *AsyncTransport) start() {
t.startOnce.Do(func() {
t.wg.Add(1)
go t.worker()
})
}
func (t *AsyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
select {
case <-t.done:
return ErrTransportClosed
default:
}
category := categoryFromEnvelope(envelope)
if t.isRateLimited(category) {
return nil
}
select {
case t.queue <- envelope:
return nil
default:
atomic.AddInt64(&t.droppedCount, 1)
return ErrTransportQueueFull
}
}
func (t *AsyncTransport) SendEvent(event protocol.EnvelopeConvertible) {
envelope, err := event.ToEnvelope(t.dsn)
if err != nil {
debuglog.Printf("Failed to convert to envelope: %v", err)
return
}
if envelope == nil {
debuglog.Printf("Error: event with empty envelope")
return
}
if err := t.SendEnvelope(envelope); err != nil {
debuglog.Printf("Error sending the envelope: %v", err)
}
}
func (t *AsyncTransport) Flush(timeout time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return t.FlushWithContext(ctx)
}
func (t *AsyncTransport) FlushWithContext(ctx context.Context) bool {
flushResponse := make(chan struct{})
select {
case t.flushRequest <- flushResponse:
select {
case <-flushResponse:
return true
case <-ctx.Done():
return false
}
case <-ctx.Done():
return false
}
}
func (t *AsyncTransport) Close() {
t.closeOnce.Do(func() {
close(t.done)
close(t.queue)
close(t.flushRequest)
t.wg.Wait()
})
}
func (t *AsyncTransport) IsRateLimited(category ratelimit.Category) bool {
return t.isRateLimited(category)
}
func (t *AsyncTransport) worker() {
defer t.wg.Done()
for {
select {
case <-t.done:
return
case envelope, open := <-t.queue:
if !open {
return
}
t.processEnvelope(envelope)
case flushResponse, open := <-t.flushRequest:
if !open {
return
}
t.drainQueue()
close(flushResponse)
}
}
}
func (t *AsyncTransport) drainQueue() {
for {
select {
case envelope, open := <-t.queue:
if !open {
return
}
t.processEnvelope(envelope)
default:
return
}
}
}
func (t *AsyncTransport) processEnvelope(envelope *protocol.Envelope) {
if t.sendEnvelopeHTTP(envelope) {
atomic.AddInt64(&t.sentCount, 1)
} else {
atomic.AddInt64(&t.errorCount, 1)
}
}
func (t *AsyncTransport) sendEnvelopeHTTP(envelope *protocol.Envelope) bool {
category := categoryFromEnvelope(envelope)
if t.isRateLimited(category) {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
if err != nil {
debuglog.Printf("Failed to create request from envelope: %v", err)
return false
}
response, err := t.client.Do(request)
if err != nil {
debuglog.Printf("HTTP request failed: %v", err)
return false
}
defer response.Body.Close()
success := t.handleResponse(response)
t.mu.Lock()
if t.limits == nil {
t.limits = make(ratelimit.Map)
}
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
return success
}
func (t *AsyncTransport) handleResponse(response *http.Response) bool {
if response.StatusCode >= 200 && response.StatusCode < 300 {
return true
}
if response.StatusCode >= 400 && response.StatusCode < 500 {
if body, err := io.ReadAll(io.LimitReader(response.Body, maxDrainResponseBytes)); err == nil {
debuglog.Printf("Client error %d: %s", response.StatusCode, string(body))
}
return false
}
if response.StatusCode >= 500 {
debuglog.Printf("Server error %d", response.StatusCode)
return false
}
debuglog.Printf("Unexpected status code %d", response.StatusCode)
return false
}
func (t *AsyncTransport) isRateLimited(category ratelimit.Category) bool {
t.mu.RLock()
defer t.mu.RUnlock()
limited := t.limits.IsRateLimited(category)
if limited {
debuglog.Printf("Rate limited for category %q until %v", category, t.limits.Deadline(category))
}
return limited
}
// NoopTransport is a transport implementation that drops all events.
// Used internally when an empty or invalid DSN is provided.
type NoopTransport struct{}
func NewNoopTransport() *NoopTransport {
debuglog.Println("Transport initialized with invalid DSN. Using NoopTransport. No events will be delivered.")
return &NoopTransport{}
}
func (t *NoopTransport) SendEnvelope(_ *protocol.Envelope) error {
debuglog.Println("Envelope dropped due to NoopTransport usage.")
return nil
}
func (t *NoopTransport) SendEvent(_ protocol.EnvelopeConvertible) {
debuglog.Println("Event dropped due to NoopTransport usage.")
}
func (t *NoopTransport) IsRateLimited(_ ratelimit.Category) bool {
return false
}
func (t *NoopTransport) Flush(_ time.Duration) bool {
return true
}
func (t *NoopTransport) FlushWithContext(_ context.Context) bool {
return true
}
func (t *NoopTransport) Close() {
// Nothing to close
}

View file

@ -0,0 +1,236 @@
package protocol
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
// apiVersion is the version of the Sentry API.
const apiVersion = "7"
type scheme string
const (
SchemeHTTP scheme = "http"
SchemeHTTPS scheme = "https"
)
func (scheme scheme) defaultPort() int {
switch scheme {
case SchemeHTTPS:
return 443
case SchemeHTTP:
return 80
default:
return 80
}
}
// DsnParseError represents an error that occurs if a Sentry
// DSN cannot be parsed.
type DsnParseError struct {
Message string
}
func (e DsnParseError) Error() string {
return "[Sentry] DsnParseError: " + e.Message
}
// Dsn is used as the remote address source to client transport.
type Dsn struct {
scheme scheme
publicKey string
secretKey string
host string
port int
path string
projectID string
}
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
// function directly. It is provided for use in custom Transport
// implementations.
func NewDsn(rawURL string) (*Dsn, error) {
// Parse
parsedURL, err := url.Parse(rawURL)
if err != nil {
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
}
// Scheme
var scheme scheme
switch parsedURL.Scheme {
case "http":
scheme = SchemeHTTP
case "https":
scheme = SchemeHTTPS
default:
return nil, &DsnParseError{"invalid scheme"}
}
// PublicKey
publicKey := parsedURL.User.Username()
if publicKey == "" {
return nil, &DsnParseError{"empty username"}
}
// SecretKey
var secretKey string
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
secretKey = parsedSecretKey
}
// Host
host := parsedURL.Hostname()
if host == "" {
return nil, &DsnParseError{"empty host"}
}
// Port
var port int
if p := parsedURL.Port(); p != "" {
port, err = strconv.Atoi(p)
if err != nil {
return nil, &DsnParseError{"invalid port"}
}
} else {
port = scheme.defaultPort()
}
// ProjectID
if parsedURL.Path == "" || parsedURL.Path == "/" {
return nil, &DsnParseError{"empty project id"}
}
pathSegments := strings.Split(parsedURL.Path[1:], "/")
projectID := pathSegments[len(pathSegments)-1]
if projectID == "" {
return nil, &DsnParseError{"empty project id"}
}
// Path
var path string
if len(pathSegments) > 1 {
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
}
return &Dsn{
scheme: scheme,
publicKey: publicKey,
secretKey: secretKey,
host: host,
port: port,
path: path,
projectID: projectID,
}, nil
}
// String formats Dsn struct into a valid string url.
func (dsn Dsn) String() string {
var url string
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
if dsn.secretKey != "" {
url += fmt.Sprintf(":%s", dsn.secretKey)
}
url += fmt.Sprintf("@%s", dsn.host)
if dsn.port != dsn.scheme.defaultPort() {
url += fmt.Sprintf(":%d", dsn.port)
}
if dsn.path != "" {
url += dsn.path
}
url += fmt.Sprintf("/%s", dsn.projectID)
return url
}
// Get the scheme of the DSN.
func (dsn Dsn) GetScheme() string {
return string(dsn.scheme)
}
// Get the public key of the DSN.
func (dsn Dsn) GetPublicKey() string {
return dsn.publicKey
}
// Get the secret key of the DSN.
func (dsn Dsn) GetSecretKey() string {
return dsn.secretKey
}
// Get the host of the DSN.
func (dsn Dsn) GetHost() string {
return dsn.host
}
// Get the port of the DSN.
func (dsn Dsn) GetPort() int {
return dsn.port
}
// Get the path of the DSN.
func (dsn Dsn) GetPath() string {
return dsn.path
}
// Get the project ID of the DSN.
func (dsn Dsn) GetProjectID() string {
return dsn.projectID
}
// GetAPIURL returns the URL of the envelope endpoint of the project
// associated with the DSN.
func (dsn Dsn) GetAPIURL() *url.URL {
var rawURL string
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
if dsn.port != dsn.scheme.defaultPort() {
rawURL += fmt.Sprintf(":%d", dsn.port)
}
if dsn.path != "" {
rawURL += dsn.path
}
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope")
parsedURL, _ := url.Parse(rawURL)
return parsedURL
}
// RequestHeaders returns all the necessary headers that have to be used in the transport when sending events
// to the /store endpoint.
//
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
// against the /envelope endpoint are added automatically.
func (dsn Dsn) RequestHeaders(sdkVersion string) map[string]string {
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), sdkVersion, dsn.publicKey)
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
}
return map[string]string{
"Content-Type": "application/json",
"X-Sentry-Auth": auth,
}
}
// MarshalJSON converts the Dsn struct to JSON.
func (dsn Dsn) MarshalJSON() ([]byte, error) {
return json.Marshal(dsn.String())
}
// UnmarshalJSON converts JSON data to the Dsn struct.
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
var str string
_ = json.Unmarshal(data, &str)
newDsn, err := NewDsn(str)
if err != nil {
return err
}
*dsn = *newDsn
return nil
}

View file

@ -0,0 +1,213 @@
package protocol
import (
"bytes"
"encoding/json"
"fmt"
"io"
"time"
)
// Envelope represents a Sentry envelope containing headers and items.
type Envelope struct {
Header *EnvelopeHeader `json:"-"`
Items []*EnvelopeItem `json:"-"`
}
// EnvelopeHeader represents the header of a Sentry envelope.
type EnvelopeHeader struct {
// EventID is the unique identifier for this event
EventID string `json:"event_id"`
// SentAt is the timestamp when the event was sent from the SDK as string in RFC 3339 format.
// Used for clock drift correction of the event timestamp. The time zone must be UTC.
SentAt time.Time `json:"sent_at,omitempty"`
// Dsn can be used for self-authenticated envelopes.
// This means that the envelope has all the information necessary to be sent to sentry.
// In this case the full DSN must be stored in this key.
Dsn string `json:"dsn,omitempty"`
// Sdk carries the same payload as the sdk interface in the event payload but can be carried for all events.
// This means that SDK information can be carried for minidumps, session data and other submissions.
Sdk *SdkInfo `json:"sdk,omitempty"`
// Trace contains the [Dynamic Sampling Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/)
Trace map[string]string `json:"trace,omitempty"`
}
// EnvelopeItemType represents the type of envelope item.
type EnvelopeItemType string
// Constants for envelope item types as defined in the Sentry documentation.
const (
EnvelopeItemTypeEvent EnvelopeItemType = "event"
EnvelopeItemTypeTransaction EnvelopeItemType = "transaction"
EnvelopeItemTypeCheckIn EnvelopeItemType = "check_in"
EnvelopeItemTypeAttachment EnvelopeItemType = "attachment"
EnvelopeItemTypeLog EnvelopeItemType = "log"
)
// EnvelopeItemHeader represents the header of an envelope item.
type EnvelopeItemHeader struct {
// Type specifies the type of this Item and its contents.
// Based on the Item type, more headers may be required.
Type EnvelopeItemType `json:"type"`
// Length is the length of the payload in bytes.
// If no length is specified, the payload implicitly goes to the next newline.
// For payloads containing newline characters, the length must be specified.
Length *int `json:"length,omitempty"`
// Filename is the name of the attachment file (used for attachments)
Filename string `json:"filename,omitempty"`
// ContentType is the MIME type of the item payload (used for attachments and some other item types)
ContentType string `json:"content_type,omitempty"`
// ItemCount is the number of items in a batch (used for logs)
ItemCount *int `json:"item_count,omitempty"`
}
// EnvelopeItem represents a single item within an envelope.
type EnvelopeItem struct {
Header *EnvelopeItemHeader `json:"-"`
Payload []byte `json:"-"`
}
// NewEnvelope creates a new envelope with the given header.
func NewEnvelope(header *EnvelopeHeader) *Envelope {
return &Envelope{
Header: header,
Items: make([]*EnvelopeItem, 0),
}
}
// AddItem adds an item to the envelope.
func (e *Envelope) AddItem(item *EnvelopeItem) {
e.Items = append(e.Items, item)
}
// Serialize serializes the envelope to the Sentry envelope format.
//
// Format: Headers "\n" { Item } [ "\n" ]
// Item: Headers "\n" Payload "\n".
func (e *Envelope) Serialize() ([]byte, error) {
var buf bytes.Buffer
headerBytes, err := json.Marshal(e.Header)
if err != nil {
return nil, fmt.Errorf("failed to marshal envelope header: %w", err)
}
if _, err := buf.Write(headerBytes); err != nil {
return nil, fmt.Errorf("failed to write envelope header: %w", err)
}
if _, err := buf.WriteString("\n"); err != nil {
return nil, fmt.Errorf("failed to write newline after envelope header: %w", err)
}
for _, item := range e.Items {
if err := e.writeItem(&buf, item); err != nil {
return nil, fmt.Errorf("failed to write envelope item: %w", err)
}
}
return buf.Bytes(), nil
}
// WriteTo writes the envelope to the given writer in the Sentry envelope format.
func (e *Envelope) WriteTo(w io.Writer) (int64, error) {
data, err := e.Serialize()
if err != nil {
return 0, err
}
n, err := w.Write(data)
return int64(n), err
}
// writeItem writes a single envelope item to the buffer.
func (e *Envelope) writeItem(buf *bytes.Buffer, item *EnvelopeItem) error {
headerBytes, err := json.Marshal(item.Header)
if err != nil {
return fmt.Errorf("failed to marshal item header: %w", err)
}
if _, err := buf.Write(headerBytes); err != nil {
return fmt.Errorf("failed to write item header: %w", err)
}
if _, err := buf.WriteString("\n"); err != nil {
return fmt.Errorf("failed to write newline after item header: %w", err)
}
if len(item.Payload) > 0 {
if _, err := buf.Write(item.Payload); err != nil {
return fmt.Errorf("failed to write item payload: %w", err)
}
}
if _, err := buf.WriteString("\n"); err != nil {
return fmt.Errorf("failed to write newline after item payload: %w", err)
}
return nil
}
// Size returns the total size of the envelope when serialized.
func (e *Envelope) Size() (int, error) {
data, err := e.Serialize()
if err != nil {
return 0, err
}
return len(data), nil
}
// MarshalJSON converts the EnvelopeHeader to JSON.
func (h *EnvelopeHeader) MarshalJSON() ([]byte, error) {
type header EnvelopeHeader
return json.Marshal((*header)(h))
}
// NewEnvelopeItem creates a new envelope item with the specified type and payload.
func NewEnvelopeItem(itemType EnvelopeItemType, payload []byte) *EnvelopeItem {
length := len(payload)
return &EnvelopeItem{
Header: &EnvelopeItemHeader{
Type: itemType,
Length: &length,
},
Payload: payload,
}
}
// NewAttachmentItem creates a new envelope item for an attachment.
// Parameters: filename, contentType, payload.
func NewAttachmentItem(filename, contentType string, payload []byte) *EnvelopeItem {
length := len(payload)
return &EnvelopeItem{
Header: &EnvelopeItemHeader{
Type: EnvelopeItemTypeAttachment,
Length: &length,
ContentType: contentType,
Filename: filename,
},
Payload: payload,
}
}
// NewLogItem creates a new envelope item for logs.
func NewLogItem(itemCount int, payload []byte) *EnvelopeItem {
length := len(payload)
return &EnvelopeItem{
Header: &EnvelopeItemHeader{
Type: EnvelopeItemTypeLog,
Length: &length,
ItemCount: &itemCount,
ContentType: "application/vnd.sentry.items.log+json",
},
Payload: payload,
}
}

View file

@ -0,0 +1,40 @@
package protocol
import (
"context"
"time"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
// EnvelopeConvertible represents any type that can be converted to a Sentry envelope.
// This interface allows the telemetry buffers to be generic while still working with
// concrete types like Event.
type EnvelopeConvertible interface {
// ToEnvelope converts the item to a Sentry envelope.
ToEnvelope(dsn *Dsn) (*Envelope, error)
}
// TelemetryTransport represents the envelope-first transport interface.
// This interface is designed for the telemetry buffer system and provides
// non-blocking sends with backpressure signals.
type TelemetryTransport interface {
// SendEnvelope sends an envelope to Sentry. Returns immediately with
// backpressure error if the queue is full.
SendEnvelope(envelope *Envelope) error
// SendEvent sends an event to Sentry.
SendEvent(event EnvelopeConvertible)
// IsRateLimited checks if a specific category is currently rate limited
IsRateLimited(category ratelimit.Category) bool
// Flush waits for all pending envelopes to be sent, with timeout
Flush(timeout time.Duration) bool
// FlushWithContext waits for all pending envelopes to be sent
FlushWithContext(ctx context.Context) bool
// Close shuts down the transport gracefully
Close()
}

View file

@ -0,0 +1,15 @@
package protocol
// SdkInfo contains SDK metadata.
type SdkInfo struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Integrations []string `json:"integrations,omitempty"`
Packages []SdkPackage `json:"packages,omitempty"`
}
// SdkPackage describes a package that was installed.
type SdkPackage struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}

View file

@ -57,3 +57,47 @@ func (c Category) String() string {
return rv
}
}
// Priority represents the importance level of a category for buffer management.
type Priority int
const (
PriorityCritical Priority = iota + 1
PriorityHigh
PriorityMedium
PriorityLow
PriorityLowest
)
func (p Priority) String() string {
switch p {
case PriorityCritical:
return "critical"
case PriorityHigh:
return "high"
case PriorityMedium:
return "medium"
case PriorityLow:
return "low"
case PriorityLowest:
return "lowest"
default:
return "unknown"
}
}
// GetPriority returns the priority level for this category.
func (c Category) GetPriority() Priority {
switch c {
case CategoryError:
return PriorityCritical
case CategoryMonitor:
return PriorityHigh
case CategoryLog:
return PriorityMedium
case CategoryTransaction:
return PriorityLow
default:
return PriorityMedium
}
}

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/getsentry/sentry-go/attribute"
"github.com/getsentry/sentry-go/internal/debuglog"
)
type LogLevel string
@ -74,7 +75,7 @@ func NewLogger(ctx context.Context) Logger {
}
}
DebugLogger.Println("fallback to noopLogger: enableLogs disabled")
debuglog.Println("fallback to noopLogger: enableLogs disabled")
return &noopLogger{} // fallback: does nothing
}
@ -89,20 +90,30 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me
if message == "" {
return
}
hub := GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub()
}
var traceID TraceID
var spanID SpanID
var span *Span
var user User
span = SpanFromContext(ctx)
if span == nil {
span = SpanFromContext(l.ctx)
}
hub := GetHubFromContext(ctx)
if hub == nil {
hub = GetHubFromContext(l.ctx)
}
if hub == nil {
hub = CurrentHub()
}
scope := hub.Scope()
if scope != nil {
scope.mu.Lock()
// Use span from hub only as last resort
if span == nil {
span = scope.span
}
if span != nil {
traceID = span.TraceID
spanID = span.SpanID
@ -187,7 +198,7 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me
}
if l.client.options.Debug {
DebugLogger.Printf(message, args...)
debuglog.Printf(message, args...)
}
}
@ -198,7 +209,7 @@ func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) {
for _, v := range attrs {
t, ok := mapTypesToStr[v.Value.Type()]
if !ok || t == "" {
DebugLogger.Printf("invalid attribute type set: %v", t)
debuglog.Printf("invalid attribute type set: %v", t)
continue
}

View file

@ -6,6 +6,7 @@ import (
"os"
"github.com/getsentry/sentry-go/attribute"
"github.com/getsentry/sentry-go/internal/debuglog"
)
// Fallback, no-op logger if logging is disabled.
@ -46,7 +47,7 @@ func (n *noopLogEntry) Attributes(_ ...attribute.Builder) LogEntry {
}
func (n *noopLogEntry) Emit(args ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level)
debuglog.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level)
if n.level == LogLevelFatal {
if n.shouldPanic {
panic(args)
@ -56,7 +57,7 @@ func (n *noopLogEntry) Emit(args ...interface{}) {
}
func (n *noopLogEntry) Emitf(message string, args ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level)
debuglog.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level)
if n.level == LogLevelFatal {
if n.shouldPanic {
panic(fmt.Sprintf(message, args...))
@ -96,7 +97,7 @@ func (*noopLogger) Panic() LogEntry {
}
func (*noopLogger) SetAttributes(...attribute.Builder) {
DebugLogger.Printf("No attributes attached. Turn on logging via EnableLogs")
debuglog.Printf("No attributes attached. Turn on logging via EnableLogs")
}
func (*noopLogger) Write(_ []byte) (n int, err error) {

View file

@ -6,6 +6,8 @@ import (
"net/http"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
)
// Scope holds contextual data for the current scope.
@ -472,7 +474,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client)
id := event.EventID
event = processor(event, hint)
if event == nil {
DebugLogger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
debuglog.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
return nil
}
}

View file

@ -6,7 +6,7 @@ import (
)
// The version of the SDK.
const SDKVersion = "0.35.3"
const SDKVersion = "0.36.2"
// apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK.

View file

@ -2,6 +2,8 @@ package sentry
import (
"sync"
"github.com/getsentry/sentry-go/internal/debuglog"
)
// A spanRecorder stores a span tree that makes up a transaction. Safe for
@ -24,7 +26,7 @@ func (r *spanRecorder) record(s *Span) {
if len(r.spans) >= maxSpans {
r.overflowOnce.Do(func() {
root := r.spans[0]
DebugLogger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
debuglog.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
root.TraceID, root.SpanID, maxSpans)
})
// TODO(tracing): mark the transaction event in some way to

View file

@ -12,6 +12,8 @@ import (
"strings"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
)
const (
@ -347,6 +349,58 @@ func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) {
}
}
// shouldIgnoreStatusCode checks if the transaction should be ignored based on HTTP status code.
func (s *Span) shouldIgnoreStatusCode() bool {
if !s.IsTransaction() {
return false
}
ignoreStatusCodes := s.clientOptions().TraceIgnoreStatusCodes
if len(ignoreStatusCodes) == 0 {
return false
}
s.mu.Lock()
statusCodeData, exists := s.Data["http.response.status_code"]
s.mu.Unlock()
if !exists {
return false
}
statusCode, ok := statusCodeData.(int)
if !ok {
return false
}
for _, ignoredRange := range ignoreStatusCodes {
switch len(ignoredRange) {
case 1:
// Single status code
if statusCode == ignoredRange[0] {
s.mu.Lock()
s.Sampled = SampledFalse
s.mu.Unlock()
debuglog.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes", statusCode)
return true
}
case 2:
// Range of status codes [min, max]
if ignoredRange[0] <= statusCode && statusCode <= ignoredRange[1] {
s.mu.Lock()
s.Sampled = SampledFalse
s.mu.Unlock()
debuglog.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes range [%d, %d]", statusCode, ignoredRange[0], ignoredRange[1])
return true
}
default:
debuglog.Printf("incorrect TraceIgnoreStatusCodes format: %v", ignoredRange)
}
}
return false
}
// doFinish runs the actual Span.Finish() logic.
func (s *Span) doFinish() {
if s.EndTime.IsZero() {
@ -360,6 +414,10 @@ func (s *Span) doFinish() {
}
}
if s.shouldIgnoreStatusCode() {
return
}
if !s.Sampled.Bool() {
return
}
@ -449,14 +507,14 @@ func (s *Span) sample() Sampled {
// https://develop.sentry.dev/sdk/performance/#sampling
// #1 tracing is not enabled.
if !clientOptions.EnableTracing {
DebugLogger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
debuglog.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
s.sampleRate = 0.0
return SampledFalse
}
// #2 explicit sampling decision via StartSpan/StartTransaction options.
if s.explicitSampled != SampledUndefined {
DebugLogger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.explicitSampled)
debuglog.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.explicitSampled)
switch s.explicitSampled {
case SampledTrue:
s.sampleRate = 1.0
@ -489,25 +547,25 @@ func (s *Span) sample() Sampled {
s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(tracesSamplerSampleRate, 'f', -1, 64)
}
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
DebugLogger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
debuglog.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
return SampledFalse
}
if tracesSamplerSampleRate == 0.0 {
DebugLogger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
debuglog.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
return SampledFalse
}
if rng.Float64() < tracesSamplerSampleRate {
return SampledTrue
}
DebugLogger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
debuglog.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
return SampledFalse
}
// #4 inherit parent decision.
if s.Sampled != SampledUndefined {
DebugLogger.Printf("Using sampling decision from parent: %v", s.Sampled)
debuglog.Printf("Using sampling decision from parent: %v", s.Sampled)
switch s.Sampled {
case SampledTrue:
s.sampleRate = 1.0
@ -525,11 +583,11 @@ func (s *Span) sample() Sampled {
s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}
if sampleRate < 0.0 || sampleRate > 1.0 {
DebugLogger.Printf("Dropping transaction: TracesSampleRate out of range [0.0, 1.0]: %f", sampleRate)
debuglog.Printf("Dropping transaction: TracesSampleRate out of range [0.0, 1.0]: %f", sampleRate)
return SampledFalse
}
if sampleRate == 0.0 {
DebugLogger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
debuglog.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
return SampledFalse
}
@ -552,7 +610,7 @@ func (s *Span) toEvent() *Event {
finished := make([]*Span, 0, len(children))
for _, child := range children {
if child.EndTime.IsZero() {
DebugLogger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
debuglog.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
continue
}
finished = append(finished, child)

View file

@ -13,6 +13,9 @@ import (
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
httpinternal "github.com/getsentry/sentry-go/internal/http"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
@ -87,14 +90,14 @@ func getRequestBodyFromEvent(event *Event) []byte {
}
body, err = json.Marshal(event)
if err == nil {
DebugLogger.Println(msg)
debuglog.Println(msg)
return body
}
// This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable
// Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry)
// Juuust in case something, somehow goes utterly wrong.
DebugLogger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
debuglog.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
"Please notify the SDK owners with possibly broken payload.")
return nil
}
@ -227,13 +230,13 @@ func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.R
r.Header.Set("Content-Type", "application/x-sentry-envelope")
auth := fmt.Sprintf("Sentry sentry_version=%s, "+
"sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.publicKey)
"sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.GetPublicKey())
// The key sentry_secret is effectively deprecated and no longer needs to be set.
// However, since it was required in older self-hosted versions,
// it should still passed through to Sentry if set.
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
if dsn.GetSecretKey() != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey())
}
r.Header.Set("X-Sentry-Auth", auth)
@ -321,7 +324,7 @@ func NewHTTPTransport() *HTTPTransport {
func (t *HTTPTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn)
if err != nil {
DebugLogger.Printf("%v\n", err)
debuglog.Printf("%v\n", err)
return
}
t.dsn = dsn
@ -403,17 +406,17 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
if event.Type == transactionType {
eventType = "transaction"
} else {
eventType = fmt.Sprintf("%s event", event.Level)
eventType = fmt.Sprintf("%s event", event.Type)
}
DebugLogger.Printf(
debuglog.Printf(
"Sending %s [%s] to %s project: %s",
eventType,
event.EventID,
t.dsn.host,
t.dsn.projectID,
t.dsn.GetHost(),
t.dsn.GetProjectID(),
)
default:
DebugLogger.Println("Event dropped due to transport buffer being full.")
debuglog.Println("Event dropped due to transport buffer being full.")
}
t.buffer <- b
@ -477,14 +480,14 @@ started:
// Wait until the current batch is done or the timeout.
select {
case <-b.done:
DebugLogger.Println("Buffer flushed successfully.")
debuglog.Println("Buffer flushed successfully.")
return true
case <-timeout:
goto fail
}
fail:
DebugLogger.Println("Buffer flushing was canceled or timed out.")
debuglog.Println("Buffer flushing was canceled or timed out.")
return false
}
@ -524,15 +527,15 @@ func (t *HTTPTransport) worker() {
response, err := t.client.Do(item.request)
if err != nil {
DebugLogger.Printf("There was an issue with sending an event: %v", err)
debuglog.Printf("There was an issue with sending an event: %v", err)
continue
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
DebugLogger.Printf("Error while reading response code: %v", err)
debuglog.Printf("Error while reading response code: %v", err)
}
DebugLogger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
debuglog.Printf("Sending %s failed with the following error: %s", eventType, string(b))
}
t.mu.Lock()
@ -559,7 +562,7 @@ func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
defer t.mu.RUnlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
DebugLogger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
@ -605,7 +608,7 @@ func NewHTTPSyncTransport() *HTTPSyncTransport {
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn)
if err != nil {
DebugLogger.Printf("%v\n", err)
debuglog.Printf("%v\n", err)
return
}
t.dsn = dsn
@ -660,25 +663,25 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
default:
eventIdentifier = fmt.Sprintf("%s event", event.Level)
}
DebugLogger.Printf(
debuglog.Printf(
"Sending %s [%s] to %s project: %s",
eventIdentifier,
event.EventID,
t.dsn.host,
t.dsn.projectID,
t.dsn.GetHost(),
t.dsn.GetProjectID(),
)
response, err := t.client.Do(request)
if err != nil {
DebugLogger.Printf("There was an issue with sending an event: %v", err)
debuglog.Printf("There was an issue with sending an event: %v", err)
return
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
DebugLogger.Printf("Error while reading response code: %v", err)
debuglog.Printf("Error while reading response code: %v", err)
}
DebugLogger.Printf("Sending %s failed with the following error: %s", eventIdentifier, string(b))
debuglog.Printf("Sending %s failed with the following error: %s", eventIdentifier, string(b))
}
t.mu.Lock()
@ -710,7 +713,7 @@ func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool {
defer t.mu.Unlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
DebugLogger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
@ -726,11 +729,11 @@ type noopTransport struct{}
var _ Transport = noopTransport{}
func (noopTransport) Configure(ClientOptions) {
DebugLogger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
debuglog.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
}
func (noopTransport) SendEvent(*Event) {
DebugLogger.Println("Event dropped due to noopTransport usage.")
debuglog.Println("Event dropped due to noopTransport usage.")
}
func (noopTransport) Flush(time.Duration) bool {
@ -742,3 +745,60 @@ func (noopTransport) FlushWithContext(context.Context) bool {
}
func (noopTransport) Close() {}
// ================================
// Internal Transport Adapters
// ================================
// newInternalAsyncTransport creates a new AsyncTransport from internal/http
// wrapped to satisfy the Transport interface.
//
// This is not yet exposed in the public API and is for internal experimentation.
func newInternalAsyncTransport() Transport {
return &internalAsyncTransportAdapter{}
}
// internalAsyncTransportAdapter wraps the internal AsyncTransport to implement
// the root-level Transport interface.
type internalAsyncTransportAdapter struct {
transport protocol.TelemetryTransport
dsn *protocol.Dsn
}
func (a *internalAsyncTransportAdapter) Configure(options ClientOptions) {
transportOptions := httpinternal.TransportOptions{
Dsn: options.Dsn,
HTTPClient: options.HTTPClient,
HTTPTransport: options.HTTPTransport,
HTTPProxy: options.HTTPProxy,
HTTPSProxy: options.HTTPSProxy,
CaCerts: options.CaCerts,
}
a.transport = httpinternal.NewAsyncTransport(transportOptions)
if options.Dsn != "" {
dsn, err := protocol.NewDsn(options.Dsn)
if err != nil {
debuglog.Printf("Failed to parse DSN in adapter: %v\n", err)
} else {
a.dsn = dsn
}
}
}
func (a *internalAsyncTransportAdapter) SendEvent(event *Event) {
a.transport.SendEvent(event)
}
func (a *internalAsyncTransportAdapter) Flush(timeout time.Duration) bool {
return a.transport.Flush(timeout)
}
func (a *internalAsyncTransportAdapter) FlushWithContext(ctx context.Context) bool {
return a.transport.FlushWithContext(ctx)
}
func (a *internalAsyncTransportAdapter) Close() {
a.transport.Close()
}

View file

@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
exec "golang.org/x/sys/execabs"
)
@ -62,7 +63,7 @@ func defaultRelease() (release string) {
}
for _, e := range envs {
if release = os.Getenv(e); release != "" {
DebugLogger.Printf("Using release from environment variable %s: %s", e, release)
debuglog.Printf("Using release from environment variable %s: %s", e, release)
return release
}
}
@ -89,23 +90,23 @@ func defaultRelease() (release string) {
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr)
}
DebugLogger.Print(s.String())
debuglog.Print(s.String())
} else {
release = strings.TrimSpace(string(b))
DebugLogger.Printf("Using release from Git: %s", release)
debuglog.Printf("Using release from Git: %s", release)
return release
}
}
DebugLogger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
DebugLogger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
debuglog.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
debuglog.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
}
func revisionFromBuildInfo(info *debug.BuildInfo) string {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && setting.Value != "" {
DebugLogger.Printf("Using release from debug info: %s", setting.Value)
debuglog.Printf("Using release from debug info: %s", setting.Value)
return setting.Value
}
}

View file

@ -163,7 +163,7 @@ func (o *Inspector) structToMap(val reflect.Value) map[string]interface{} {
case reflect.Bool:
result[fieldName] = *(*bool)(ptr)
default:
result[fieldName] = fmt.Sprintf("<unexported %s>", field.Type().String())
result[fieldName] = fmt.Sprintf("*unexported %s*", field.Type().String())
}
}
}

View file

@ -495,7 +495,7 @@ func (l *Logger) Fatal(args ...any) {
os.Exit(1)
}
l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, true)
l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, false)
os.Exit(1)
}

View file

@ -11,9 +11,16 @@ import (
var Functions = make(map[string]any)
func init() {
sql.Register("litestream", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) (err error) {
func setPragmas(conn *sqlite3.SQLiteConn, pragmas ...string) (err error) {
for _, pragma := range pragmas {
if _, err = conn.Exec(pragma, []driver.Value{}); err != nil {
return
}
}
return
}
func registerFuncsAndSetTrace(conn *sqlite3.SQLiteConn) (err error) {
for name, fn := range Functions {
err = conn.RegisterFunc(name, fn, true)
if err != nil {
@ -21,55 +28,58 @@ func init() {
}
}
err = DoSetTrace(conn)
if err != nil {
return
}
func init() {
sql.Register("litestream", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) (err error) {
if err = registerFuncsAndSetTrace(conn); err != nil {
return
}
if err = conn.SetFileControlInt("main", sqlite3.SQLITE_FCNTL_PERSIST_WAL, 1); err != nil {
return
}
if _, err = conn.Exec("PRAGMA foreign_keys = ON", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA journal_mode = WAL", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA wal_autocheckpoint = 0", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA synchronous = NORMAL", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA busy_timeout = 5000", []driver.Value{}); err != nil {
return
}
err = setPragmas(
conn,
"PRAGMA foreign_keys = ON",
"PRAGMA journal_mode = WAL",
"PRAGMA wal_autocheckpoint = 0",
"PRAGMA synchronous = NORMAL",
"PRAGMA busy_timeout = 5000",
)
return
},
})
sql.Register("sqlite3-fk-wal", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) (err error) {
for name, fn := range Functions {
err = conn.RegisterFunc(name, fn, true)
if err != nil {
if err = registerFuncsAndSetTrace(conn); err != nil {
return
}
}
err = DoSetTrace(conn)
if err != nil {
return
}
if _, err = conn.Exec("PRAGMA foreign_keys = ON", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA journal_mode = WAL", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA synchronous = NORMAL", []driver.Value{}); err != nil {
return
}
if _, err = conn.Exec("PRAGMA busy_timeout = 5000", []driver.Value{}); err != nil {
err = setPragmas(
conn,
"PRAGMA foreign_keys = ON",
"PRAGMA journal_mode = WAL",
"PRAGMA synchronous = NORMAL",
"PRAGMA busy_timeout = 5000",
)
return
},
})
sql.Register("sqlite3-fk-wal-fullsync", &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) (err error) {
if err = registerFuncsAndSetTrace(conn); err != nil {
return
}
err = setPragmas(
conn,
"PRAGMA foreign_keys = ON",
"PRAGMA journal_mode = WAL",
"PRAGMA synchronous = FULL",
"PRAGMA busy_timeout = 5000",
)
return
},
})

View file

@ -114,5 +114,6 @@
// whole file is limited to that dialect.
//
// If the filter ends with `(lines commented)`, then ALL lines chosen
// by the filter will be uncommented.
// by the filter will be uncommented. The `--` comment prefix must be
// at the beginning of the line with no whitespace ahead of it.
package dbutil

View file

@ -173,6 +173,9 @@ func (db *Database) filterSQLUpgrade(lines [][]byte) (string, error) {
}
if dialect == db.Dialect {
if uncomment {
if !bytes.HasPrefix(lines[i], []byte("--")) {
return "", fmt.Errorf("line %d isn't commented even though the dialect filter claimed it is (-- must be at beginning of line)", i+1)
}
output = append(output, bytes.TrimPrefix(lines[i], []byte("--")))
} else {
output = append(output, lines[i])

19
vendor/go.mau.fi/util/exmaps/clone.go vendored Normal file
View file

@ -0,0 +1,19 @@
// Copyright (c) 2025 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package exmaps
import (
"maps"
)
// NonNilClone returns a copy of the given map, or an empty map if the input is nil.
func NonNilClone[Key comparable, Value any](m map[Key]Value) map[Key]Value {
if m == nil {
return make(map[Key]Value)
}
return maps.Clone(m)
}

View file

@ -6,7 +6,11 @@
package exsync
import "sync"
import (
"iter"
"maps"
"sync"
)
// Map is a simple map with a built-in mutex.
type Map[Key comparable, Value any] struct {
@ -15,8 +19,13 @@ type Map[Key comparable, Value any] struct {
}
func NewMap[Key comparable, Value any]() *Map[Key, Value] {
return NewMapWithData(make(map[Key]Value))
}
// NewMapWithData constructs a Map with the given map as data. Accessing the map directly after passing it here is not safe.
func NewMapWithData[Key comparable, Value any](data map[Key]Value) *Map[Key, Value] {
return &Map[Key, Value]{
data: make(map[Key]Value),
data: data,
}
}
@ -77,18 +86,50 @@ func (sm *Map[Key, Value]) GetOrSet(key Key, value Value) (actual Value, wasGet
return
}
// Clear removes all items from the map.
func (sm *Map[Key, Value]) Clear() {
sm.lock.Lock()
clear(sm.data)
sm.lock.Unlock()
}
// Len returns the number of items in the map.
func (sm *Map[Key, Value]) Len() int {
sm.lock.RLock()
l := len(sm.data)
sm.lock.RUnlock()
return l
}
// CopyFrom copies all key/value pairs from the given map into this map, overriding any existing keys.
// Keys present in this map but not in the given map are not removed.
func (sm *Map[Key, Value]) CopyFrom(other map[Key]Value) {
sm.lock.Lock()
maps.Copy(sm.data, other)
sm.lock.Unlock()
}
// Clone returns a copy of the map.
func (sm *Map[Key, Value]) Clone() *Map[Key, Value] {
return &Map[Key, Value]{data: sm.CopyData()}
return NewMapWithData(sm.CopyData())
}
// CopyData returns a copy of the data in the map as a normal (non-atomic) map.
func (sm *Map[Key, Value]) CopyData() map[Key]Value {
sm.lock.RLock()
copied := make(map[Key]Value, len(sm.data))
for key, value := range sm.data {
copied[key] = value
}
copied := maps.Clone(sm.data)
sm.lock.RUnlock()
return copied
}
func (sm *Map[Key, Value]) Iter() iter.Seq2[Key, Value] {
return func(yield func(Key, Value) bool) {
sm.lock.RLock()
defer sm.lock.RUnlock()
for k, v := range sm.data {
if !yield(k, v) {
return
}
}
}
}

View file

@ -7,6 +7,7 @@
package exsync
import (
"iter"
"sync"
)
@ -134,3 +135,15 @@ func (s *Set[T]) AsList() []T {
s.l.RUnlock()
return list
}
func (s *Set[T]) Iter() iter.Seq[T] {
return func(yield func(T) bool) {
s.l.RLock()
defer s.l.RUnlock()
for item := range s.m {
if !yield(item) {
return
}
}
}
}

View file

@ -10,12 +10,19 @@ import (
"time"
)
func zeroSafeUnixToTime(val int64, fn func(int64) time.Time) time.Time {
if val == 0 {
return time.Time{}
}
return fn(val)
}
func UM(time time.Time) UnixMilli {
return UnixMilli{Time: time}
}
func UMInt(ts int64) UnixMilli {
return UM(time.UnixMilli(ts))
return UM(zeroSafeUnixToTime(ts, time.UnixMilli))
}
func UnixMilliNow() UnixMilli {
@ -26,8 +33,8 @@ func UMicro(time time.Time) UnixMicro {
return UnixMicro{Time: time}
}
func UMicroInto(ts int64) UnixMicro {
return UMicro(time.UnixMicro(ts))
func UMicroInt(ts int64) UnixMicro {
return UMicro(zeroSafeUnixToTime(ts, time.UnixMicro))
}
func UnixMicroNow() UnixMicro {
@ -39,7 +46,9 @@ func UN(time time.Time) UnixNano {
}
func UNInt(ts int64) UnixNano {
return UN(time.Unix(0, ts))
return UN(zeroSafeUnixToTime(ts, func(i int64) time.Time {
return time.Unix(0, i)
}))
}
func UnixNanoNow() UnixNano {
@ -51,7 +60,9 @@ func U(time time.Time) Unix {
}
func UInt(ts int64) Unix {
return U(time.Unix(ts, 0))
return U(zeroSafeUnixToTime(ts, func(i int64) time.Time {
return time.Unix(i, 0)
}))
}
func UnixNow() Unix {

View file

@ -57,6 +57,13 @@ func anyIntegerToTime(src any, unixConv func(int64) time.Time, into *time.Time)
return nil
}
func zeroSafeUnix(t time.Time, method func(time.Time) int64) int64 {
if t.IsZero() {
return 0
}
return method(t)
}
var _ sql.Scanner = &UnixMilli{}
var _ driver.Valuer = UnixMilli{}
@ -65,9 +72,6 @@ type UnixMilli struct {
}
func (um UnixMilli) MarshalJSON() ([]byte, error) {
if um.IsZero() {
return []byte{'0'}, nil
}
return json.Marshal(um.UnixMilli())
}
@ -83,6 +87,11 @@ func (um *UnixMilli) Scan(src any) error {
return anyIntegerToTime(src, time.UnixMilli, &um.Time)
}
func (um UnixMilli) Unix() int64 { return zeroSafeUnix(um.Time, time.Time.Unix) }
func (um UnixMilli) UnixMilli() int64 { return zeroSafeUnix(um.Time, time.Time.UnixMilli) }
func (um UnixMilli) UnixMicro() int64 { return zeroSafeUnix(um.Time, time.Time.UnixMicro) }
func (um UnixMilli) UnixNano() int64 { return zeroSafeUnix(um.Time, time.Time.UnixNano) }
var _ sql.Scanner = &UnixMicro{}
var _ driver.Valuer = UnixMicro{}
@ -91,9 +100,6 @@ type UnixMicro struct {
}
func (um UnixMicro) MarshalJSON() ([]byte, error) {
if um.IsZero() {
return []byte{'0'}, nil
}
return json.Marshal(um.UnixMicro())
}
@ -109,6 +115,11 @@ func (um *UnixMicro) Scan(src any) error {
return anyIntegerToTime(src, time.UnixMicro, &um.Time)
}
func (um UnixMicro) Unix() int64 { return zeroSafeUnix(um.Time, time.Time.Unix) }
func (um UnixMicro) UnixMilli() int64 { return zeroSafeUnix(um.Time, time.Time.UnixMilli) }
func (um UnixMicro) UnixMicro() int64 { return zeroSafeUnix(um.Time, time.Time.UnixMicro) }
func (um UnixMicro) UnixNano() int64 { return zeroSafeUnix(um.Time, time.Time.UnixNano) }
var _ sql.Scanner = &UnixNano{}
var _ driver.Valuer = UnixNano{}
@ -117,9 +128,6 @@ type UnixNano struct {
}
func (un UnixNano) MarshalJSON() ([]byte, error) {
if un.IsZero() {
return []byte{'0'}, nil
}
return json.Marshal(un.UnixNano())
}
@ -139,14 +147,16 @@ func (un *UnixNano) Scan(src any) error {
}, &un.Time)
}
func (un UnixNano) Unix() int64 { return zeroSafeUnix(un.Time, time.Time.Unix) }
func (un UnixNano) UnixMilli() int64 { return zeroSafeUnix(un.Time, time.Time.UnixMilli) }
func (un UnixNano) UnixMicro() int64 { return zeroSafeUnix(un.Time, time.Time.UnixMicro) }
func (un UnixNano) UnixNano() int64 { return zeroSafeUnix(un.Time, time.Time.UnixNano) }
type Unix struct {
time.Time
}
func (u Unix) MarshalJSON() ([]byte, error) {
if u.IsZero() {
return []byte{'0'}, nil
}
return json.Marshal(u.Unix())
}
@ -168,3 +178,8 @@ func (u *Unix) Scan(src any) error {
return time.Unix(i, 0)
}, &u.Time)
}
func (u Unix) Unix() int64 { return zeroSafeUnix(u.Time, time.Time.Unix) }
func (u Unix) UnixMilli() int64 { return zeroSafeUnix(u.Time, time.Time.UnixMilli) }
func (u Unix) UnixMicro() int64 { return zeroSafeUnix(u.Time, time.Time.UnixMicro) }
func (u Unix) UnixNano() int64 { return zeroSafeUnix(u.Time, time.Time.UnixNano) }

View file

@ -209,48 +209,46 @@ func DeleteImport(fset *token.FileSet, f *ast.File, path string) (deleted bool)
// DeleteNamedImport deletes the import with the given name and path from the file f, if present.
// If there are duplicate import declarations, all matching ones are deleted.
func DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (deleted bool) {
var delspecs []*ast.ImportSpec
var delcomments []*ast.CommentGroup
var (
delspecs = make(map[*ast.ImportSpec]bool)
delcomments = make(map[*ast.CommentGroup]bool)
)
// Find the import nodes that import path, if any.
for i := 0; i < len(f.Decls); i++ {
decl := f.Decls[i]
gen, ok := decl.(*ast.GenDecl)
gen, ok := f.Decls[i].(*ast.GenDecl)
if !ok || gen.Tok != token.IMPORT {
continue
}
for j := 0; j < len(gen.Specs); j++ {
spec := gen.Specs[j]
impspec := spec.(*ast.ImportSpec)
impspec := gen.Specs[j].(*ast.ImportSpec)
if importName(impspec) != name || importPath(impspec) != path {
continue
}
// We found an import spec that imports path.
// Delete it.
delspecs = append(delspecs, impspec)
delspecs[impspec] = true
deleted = true
copy(gen.Specs[j:], gen.Specs[j+1:])
gen.Specs = gen.Specs[:len(gen.Specs)-1]
gen.Specs = slices.Delete(gen.Specs, j, j+1)
// If this was the last import spec in this decl,
// delete the decl, too.
if len(gen.Specs) == 0 {
copy(f.Decls[i:], f.Decls[i+1:])
f.Decls = f.Decls[:len(f.Decls)-1]
f.Decls = slices.Delete(f.Decls, i, i+1)
i--
break
} else if len(gen.Specs) == 1 {
if impspec.Doc != nil {
delcomments = append(delcomments, impspec.Doc)
delcomments[impspec.Doc] = true
}
if impspec.Comment != nil {
delcomments = append(delcomments, impspec.Comment)
delcomments[impspec.Comment] = true
}
for _, cg := range f.Comments {
// Found comment on the same line as the import spec.
if cg.End() < impspec.Pos() && fset.Position(cg.End()).Line == fset.Position(impspec.Pos()).Line {
delcomments = append(delcomments, cg)
delcomments[cg] = true
break
}
}
@ -294,38 +292,21 @@ func DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (del
}
// Delete imports from f.Imports.
for i := 0; i < len(f.Imports); i++ {
imp := f.Imports[i]
for j, del := range delspecs {
if imp == del {
copy(f.Imports[i:], f.Imports[i+1:])
f.Imports = f.Imports[:len(f.Imports)-1]
copy(delspecs[j:], delspecs[j+1:])
delspecs = delspecs[:len(delspecs)-1]
i--
break
}
}
before := len(f.Imports)
f.Imports = slices.DeleteFunc(f.Imports, func(imp *ast.ImportSpec) bool {
_, ok := delspecs[imp]
return ok
})
if len(f.Imports)+len(delspecs) != before {
// This can happen when the AST is invalid (i.e. imports differ between f.Decls and f.Imports).
panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", delspecs))
}
// Delete comments from f.Comments.
for i := 0; i < len(f.Comments); i++ {
cg := f.Comments[i]
for j, del := range delcomments {
if cg == del {
copy(f.Comments[i:], f.Comments[i+1:])
f.Comments = f.Comments[:len(f.Comments)-1]
copy(delcomments[j:], delcomments[j+1:])
delcomments = delcomments[:len(delcomments)-1]
i--
break
}
}
}
if len(delspecs) > 0 {
panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", delspecs))
}
f.Comments = slices.DeleteFunc(f.Comments, func(cg *ast.CommentGroup) bool {
_, ok := delcomments[cg]
return ok
})
return
}

View file

@ -175,7 +175,7 @@ func ExpandPatterns(ctxt *build.Context, patterns []string) map[string]bool {
for _, pkg := range all {
doPkg(pkg, neg)
}
} else if dir := strings.TrimSuffix(arg, "/..."); dir != arg {
} else if dir, ok := strings.CutSuffix(arg, "/..."); ok {
// dir/... matches all packages beneath dir
for _, pkg := range all {
if strings.HasPrefix(pkg, dir) &&

View file

@ -43,7 +43,7 @@ func (v *TagsFlag) Set(s string) error {
// Starting in Go 1.13, the -tags flag is a comma-separated list of build tags.
*v = []string{}
for _, s := range strings.Split(s, ",") {
for s := range strings.SplitSeq(s, ",") {
if s != "" {
*v = append(*v, s)
}

View file

@ -1,3 +1,31 @@
## v0.25.2 (2025-10-16)
* **Breaking change *(id)*** Split `UserID.ParseAndValidate` into
`ParseAndValidateRelaxed` and `ParseAndValidateStrict`. Strict is the old
behavior, but most users likely want the relaxed version, as there are real
users whose user IDs aren't valid under the strict rules.
* *(crypto)* Added helper methods for generating and verifying with recovery
keys.
* *(bridgev2/matrix)* Added config option to automatically generate a recovery
key for the bridge bot and self-sign the bridge's device.
* *(bridgev2/matrix)* Added initial support for using appservice/MSC3202 mode
for encryption with standard servers like Synapse.
* *(bridgev2)* Added optional support for implicit read receipts.
* *(bridgev2)* Added interface for deleting chats on remote network.
* *(bridgev2)* Added local enforcement of media duration and size limits.
* *(bridgev2)* Extended event duration logging to log any event taking too long.
* *(bridgev2)* Improved validation in group creation provisioning API.
* *(event)* Added event type constant for poll end events.
* *(client)* Added wrapper for searching user directory.
* *(client)* Improved support for managing [MSC4140] delayed events.
* *(crypto/helper)* Changed default sync handling to not block on waiting for
decryption keys. On initial sync, keys won't be requested at all by default.
* *(crypto)* Fixed olm unwedging not working (regressed in v0.25.1).
* *(bridgev2)* Fixed various bugs with migrating to split portals.
* *(event)* Fixed poll start events having incorrect null `m.relates_to`.
* *(client)* Fixed `RespUserProfile` losing standard fields when re-marshaling.
* *(federation)* Fixed various bugs in event auth.
## v0.25.1 (2025-09-16)
* *(client)* Fixed HTTP method of delete devices API call

View file

@ -323,6 +323,7 @@ const (
LogBodyContextKey contextKey = iota
LogRequestIDContextKey
MaxAttemptsContextKey
SyncTokenContextKey
)
func (cli *Client) RequestStart(req *http.Request) {
@ -418,8 +419,18 @@ var requestID int32
var logSensitiveContent = os.Getenv("MAUTRIX_LOG_SENSITIVE_CONTENT") == "yes"
func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, error) {
reqID := atomic.AddInt32(&requestID, 1)
logger := zerolog.Ctx(ctx)
if logger.GetLevel() == zerolog.Disabled || logger == zerolog.DefaultContextLogger {
logger = params.Logger
}
ctx = logger.With().
Int32("req_id", reqID).
Logger().WithContext(ctx)
var logBody any
reqBody := params.RequestBody
var reqBody io.Reader
var reqLen int64
if params.RequestJSON != nil {
jsonStr, err := json.Marshal(params.RequestJSON)
if err != nil {
@ -434,12 +445,22 @@ func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, e
logBody = params.RequestJSON
}
reqBody = bytes.NewReader(jsonStr)
reqLen = int64(len(jsonStr))
} else if params.RequestBytes != nil {
logBody = fmt.Sprintf("<%d bytes>", len(params.RequestBytes))
reqBody = bytes.NewReader(params.RequestBytes)
params.RequestLength = int64(len(params.RequestBytes))
} else if params.RequestLength > 0 && params.RequestBody != nil {
reqLen = int64(len(params.RequestBytes))
} else if params.RequestBody != nil {
logBody = "<unknown stream of bytes>"
reqLen = -1
if params.RequestLength > 0 {
logBody = fmt.Sprintf("<%d bytes>", params.RequestLength)
reqLen = params.RequestLength
} else if params.RequestLength == 0 {
zerolog.Ctx(ctx).Warn().
Msg("RequestBody passed without specifying request length")
}
reqBody = params.RequestBody
if rsc, ok := params.RequestBody.(io.ReadSeekCloser); ok {
// Prevent HTTP from closing the request body, it might be needed for retries
reqBody = nopCloseSeeker{rsc}
@ -448,15 +469,8 @@ func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, e
params.RequestJSON = struct{}{}
logBody = params.RequestJSON
reqBody = bytes.NewReader([]byte("{}"))
reqLen = 2
}
reqID := atomic.AddInt32(&requestID, 1)
logger := zerolog.Ctx(ctx)
if logger.GetLevel() == zerolog.Disabled || logger == zerolog.DefaultContextLogger {
logger = params.Logger
}
ctx = logger.With().
Int32("req_id", reqID).
Logger().WithContext(ctx)
ctx = context.WithValue(ctx, LogBodyContextKey, logBody)
ctx = context.WithValue(ctx, LogRequestIDContextKey, int(reqID))
req, err := http.NewRequestWithContext(ctx, params.Method, params.URL, reqBody)
@ -472,9 +486,7 @@ func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, e
if params.RequestJSON != nil {
req.Header.Set("Content-Type", "application/json")
}
if params.RequestLength > 0 && params.RequestBody != nil {
req.ContentLength = params.RequestLength
}
req.ContentLength = reqLen
return req, nil
}
@ -1055,6 +1067,15 @@ func (cli *Client) GetProfile(ctx context.Context, mxid id.UserID) (resp *RespUs
return
}
func (cli *Client) SearchUserDirectory(ctx context.Context, query string, limit int) (resp *RespSearchUserDirectory, err error) {
urlPath := cli.BuildClientURL("v3", "user_directory", "search")
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, &ReqSearchUserDirectory{
SearchTerm: query,
Limit: limit,
}, &resp)
return
}
func (cli *Client) GetMutualRooms(ctx context.Context, otherUserID id.UserID, extras ...ReqMutualRooms) (resp *RespMutualRooms, err error) {
if cli.SpecVersions != nil && !cli.SpecVersions.Supports(FeatureMutualRooms) {
err = fmt.Errorf("server does not support fetching mutual rooms")
@ -1105,6 +1126,9 @@ func (cli *Client) SetDisplayName(ctx context.Context, displayName string) (err
// SetProfileField sets an arbitrary profile field. See https://spec.matrix.org/v1.16/client-server-api/#put_matrixclientv3profileuseridkeyname
func (cli *Client) SetProfileField(ctx context.Context, key string, value any) (err error) {
urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, key)
if key != "displayname" && key != "avatar_url" && !cli.SpecVersions.Supports(FeatureArbitraryProfileFields) && cli.SpecVersions.Supports(FeatureUnstableProfileFields) {
urlPath = cli.BuildClientURL("unstable", "uk.tcpip.msc4133", "profile", cli.UserID, key)
}
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, map[string]any{
key: value,
}, nil)
@ -1114,6 +1138,9 @@ func (cli *Client) SetProfileField(ctx context.Context, key string, value any) (
// DeleteProfileField deletes an arbitrary profile field. See https://spec.matrix.org/v1.16/client-server-api/#put_matrixclientv3profileuseridkeyname
func (cli *Client) DeleteProfileField(ctx context.Context, key string) (err error) {
urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, key)
if key != "displayname" && key != "avatar_url" && !cli.SpecVersions.Supports(FeatureArbitraryProfileFields) && cli.SpecVersions.Supports(FeatureUnstableProfileFields) {
urlPath = cli.BuildClientURL("unstable", "uk.tcpip.msc4133", "profile", cli.UserID, key)
}
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return
}
@ -1121,6 +1148,9 @@ func (cli *Client) DeleteProfileField(ctx context.Context, key string) (err erro
// GetProfileField gets an arbitrary profile field and parses the response into the given struct. See https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3profileuseridkeyname
func (cli *Client) GetProfileField(ctx context.Context, userID id.UserID, key string, into any) (err error) {
urlPath := cli.BuildClientURL("v3", "profile", userID, key)
if key != "displayname" && key != "avatar_url" && !cli.SpecVersions.Supports(FeatureArbitraryProfileFields) && cli.SpecVersions.Supports(FeatureUnstableProfileFields) {
urlPath = cli.BuildClientURL("unstable", "uk.tcpip.msc4133", "profile", cli.UserID, key)
}
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, into)
return
}
@ -1283,6 +1313,32 @@ func (cli *Client) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID,
return
}
func (cli *Client) DelayedEvents(ctx context.Context, req *ReqDelayedEvents) (resp *RespDelayedEvents, err error) {
query := map[string]string{}
if req.DelayID != "" {
query["delay_id"] = string(req.DelayID)
}
if req.Status != "" {
query["status"] = string(req.Status)
}
if req.NextBatch != "" {
query["next_batch"] = req.NextBatch
}
urlPath := cli.BuildURLWithQuery(ClientURLPath{"unstable", "org.matrix.msc4140", "delayed_events"}, query)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, req, &resp)
// Migration: merge old keys with new ones
if resp != nil {
resp.Scheduled = append(resp.Scheduled, resp.DelayedEvents...)
resp.DelayedEvents = nil
resp.Finalised = append(resp.Finalised, resp.FinalisedEvents...)
resp.FinalisedEvents = nil
}
return
}
func (cli *Client) UpdateDelayedEvent(ctx context.Context, req *ReqUpdateDelayedEvent) (resp *RespUpdateDelayedEvent, err error) {
urlPath := cli.BuildClientURL("unstable", "org.matrix.msc4140", "delayed_events", req.DelayID)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)

View file

@ -20,6 +20,20 @@ type CrossSigningPublicKeysCache struct {
UserSigningKey id.Ed25519
}
func (mach *OlmMachine) GetOwnVerificationStatus(ctx context.Context) (hasKeys, isVerified bool, err error) {
pubkeys := mach.GetOwnCrossSigningPublicKeys(ctx)
if pubkeys != nil {
hasKeys = true
isVerified, err = mach.CryptoStore.IsKeySignedBy(
ctx, mach.Client.UserID, mach.GetAccount().SigningKey(), mach.Client.UserID, pubkeys.SelfSigningKey,
)
if err != nil {
err = fmt.Errorf("failed to check if current device is signed by own self-signing key: %w", err)
}
}
return
}
func (mach *OlmMachine) GetOwnCrossSigningPublicKeys(ctx context.Context) *CrossSigningPublicKeysCache {
if mach.crossSigningPubkeys != nil {
return mach.crossSigningPubkeys

View file

@ -71,6 +71,42 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeysWithPassword(ctx contex
}, passphrase)
}
func (mach *OlmMachine) VerifyWithRecoveryKey(ctx context.Context, recoveryKey string) error {
keyID, keyData, err := mach.SSSS.GetDefaultKeyData(ctx)
if err != nil {
return fmt.Errorf("failed to get default SSSS key data: %w", err)
}
key, err := keyData.VerifyRecoveryKey(keyID, recoveryKey)
if err != nil {
return err
}
err = mach.FetchCrossSigningKeysFromSSSS(ctx, key)
if err != nil {
return fmt.Errorf("failed to fetch cross-signing keys from SSSS: %w", err)
}
err = mach.SignOwnDevice(ctx, mach.OwnIdentity())
if err != nil {
return fmt.Errorf("failed to sign own device: %w", err)
}
err = mach.SignOwnMasterKey(ctx)
if err != nil {
return fmt.Errorf("failed to sign own master key: %w", err)
}
return nil
}
func (mach *OlmMachine) GenerateAndVerifyWithRecoveryKey(ctx context.Context) (recoveryKey string, err error) {
recoveryKey, _, err = mach.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
if err != nil {
err = fmt.Errorf("failed to generate and upload cross-signing keys: %w", err)
} else if err = mach.SignOwnDevice(ctx, mach.OwnIdentity()); err != nil {
err = fmt.Errorf("failed to sign own device: %w", err)
} else if err = mach.SignOwnMasterKey(ctx); err != nil {
err = fmt.Errorf("failed to sign own master key: %w", err)
}
return
}
// GenerateAndUploadCrossSigningKeys generates a new key with all corresponding cross-signing keys.
//
// A passphrase can be provided to generate the SSSS key. If the passphrase is empty, a random key
@ -97,12 +133,12 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(ctx context.Context, u
// Publish cross-signing keys
err = mach.PublishCrossSigningKeys(ctx, keysCache, uiaCallback)
if err != nil {
return "", nil, fmt.Errorf("failed to publish cross-signing keys: %w", err)
return key.RecoveryKey(), keysCache, fmt.Errorf("failed to publish cross-signing keys: %w", err)
}
err = mach.SSSS.SetDefaultKeyID(ctx, key.ID)
if err != nil {
return "", nil, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
return key.RecoveryKey(), keysCache, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
}
return key.RecoveryKey(), keysCache, nil

View file

@ -225,13 +225,6 @@ func (helper *CryptoHelper) Init(ctx context.Context) error {
helper.ASEventProcessor.On(event.EventEncrypted, helper.HandleEncrypted)
}
if helper.client.SetAppServiceDeviceID {
err = helper.mach.ShareKeys(ctx, -1)
if err != nil {
return fmt.Errorf("failed to share keys: %w", err)
}
}
return nil
}
@ -268,21 +261,21 @@ func (helper *CryptoHelper) verifyDeviceKeysOnServer(ctx context.Context) error
if !ok || len(device.Keys) == 0 {
if isShared {
return fmt.Errorf("olm account is marked as shared, keys seem to have disappeared from the server")
} else {
helper.log.Debug().Msg("Olm account not shared and keys not on server, so device is probably fine")
return nil
}
helper.log.Debug().Msg("Olm account not shared and keys not on server, sharing initial keys")
err = helper.mach.ShareKeys(ctx, -1)
if err != nil {
return fmt.Errorf("failed to share keys: %w", err)
}
return nil
} else if !isShared {
return fmt.Errorf("olm account is not marked as shared, but there are keys on the server")
} else if ed := device.Keys.GetEd25519(helper.client.DeviceID); ownID.SigningKey != ed {
return fmt.Errorf("mismatching identity key on server (%q != %q)", ownID.SigningKey, ed)
}
if !isShared {
helper.log.Debug().Msg("Olm account not marked as shared, but keys on server match?")
} else {
helper.log.Debug().Msg("Olm account marked as shared and keys on server match, device is fine")
}
return nil
}
}
var NoSessionFound = crypto.NoSessionFound
@ -304,24 +297,14 @@ func (helper *CryptoHelper) HandleEncrypted(ctx context.Context, evt *event.Even
ctx = log.WithContext(ctx)
decrypted, err := helper.Decrypt(ctx, evt)
if errors.Is(err, NoSessionFound) {
log.Debug().
Int("wait_seconds", int(initialSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, waiting for keys to arrive...")
if helper.mach.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) {
log.Debug().Msg("Got keys after waiting, trying to decrypt event again")
decrypted, err = helper.Decrypt(ctx, evt)
} else {
go helper.waitLongerForSession(ctx, log, evt)
return
}
}
if err != nil {
if errors.Is(err, NoSessionFound) && ctx.Value(mautrix.SyncTokenContextKey) != "" {
go helper.waitForSession(ctx, evt)
} else if err != nil {
log.Warn().Err(err).Msg("Failed to decrypt event")
helper.DecryptErrorCallback(evt, err)
return
}
} else {
helper.postDecrypt(ctx, decrypted)
}
}
func (helper *CryptoHelper) postDecrypt(ctx context.Context, decrypted *event.Event) {
@ -362,7 +345,29 @@ func (helper *CryptoHelper) RequestSession(ctx context.Context, roomID id.RoomID
}
}
func (helper *CryptoHelper) waitLongerForSession(ctx context.Context, log zerolog.Logger, evt *event.Event) {
func (helper *CryptoHelper) waitForSession(ctx context.Context, evt *event.Event) {
log := zerolog.Ctx(ctx)
content := evt.Content.AsEncrypted()
log.Debug().
Int("wait_seconds", int(initialSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, waiting for keys to arrive...")
if helper.mach.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) {
log.Debug().Msg("Got keys after waiting, trying to decrypt event again")
decrypted, err := helper.Decrypt(ctx, evt)
if err != nil {
log.Warn().Err(err).Msg("Failed to decrypt event")
helper.DecryptErrorCallback(evt, err)
} else {
helper.postDecrypt(ctx, decrypted)
}
} else {
go helper.waitLongerForSession(ctx, evt)
}
}
func (helper *CryptoHelper) waitLongerForSession(ctx context.Context, evt *event.Event) {
log := zerolog.Ctx(ctx)
content := evt.Content.AsEncrypted()
log.Debug().Int("wait_seconds", int(extendedSessionWaitTimeout.Seconds())).Msg("Couldn't find session, requesting keys and waiting longer...")

View file

@ -729,7 +729,7 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro
start := time.Now()
mach.otkUploadLock.Lock()
defer mach.otkUploadLock.Unlock()
if mach.lastOTKUpload.Add(1*time.Minute).After(start) || currentOTKCount < 0 {
if mach.lastOTKUpload.Add(1*time.Minute).After(start) || (currentOTKCount < 0 && mach.account.Shared) {
log.Debug().Msg("Checking OTK count from server due to suspiciously close share keys requests or negative OTK count")
resp, err := mach.Client.UploadKeys(ctx, &mautrix.ReqUploadKeys{})
if err != nil {

View file

@ -253,7 +253,7 @@ func (store *SQLCryptoStore) GetLatestSession(ctx context.Context, key id.Sender
// GetNewestSessionCreationTS gets the creation timestamp of the most recently created session with the given sender key.
// This will exclude sessions that have never been used to encrypt or decrypt a message.
func (store *SQLCryptoStore) GetNewestSessionCreationTS(ctx context.Context, key id.SenderKey) (createdAt time.Time, err error) {
err = store.DB.QueryRow(ctx, "SELECT created_at FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 AND (encrypted_at <> created_at OR decrypted_at <> created_at) ORDER BY created_at DESC LIMIT 1",
err = store.DB.QueryRow(ctx, "SELECT created_at FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 AND (last_encrypted <> created_at OR last_decrypted <> created_at) ORDER BY created_at DESC LIMIT 1",
key, store.AccountID).Scan(&createdAt)
if errors.Is(err, sql.ErrNoRows) {
err = nil
@ -664,6 +664,20 @@ func (store *SQLCryptoStore) IsOutboundGroupSessionShared(ctx context.Context, u
// ValidateMessageIndex returns whether the given event information match the ones stored in the database
// for the given sender key, session ID and index. If the index hasn't been stored, this will store it.
func (store *SQLCryptoStore) ValidateMessageIndex(ctx context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
if eventID == "" && timestamp == 0 {
var notOK bool
const validateEmptyQuery = `
SELECT EXISTS(SELECT 1 FROM crypto_message_index WHERE sender_key=$1 AND session_id=$2 AND "index"=$3)
`
err := store.DB.QueryRow(ctx, validateEmptyQuery, senderKey, sessionID, index).Scan(&notOK)
if notOK {
zerolog.Ctx(ctx).Debug().
Uint("message_index", index).
Msg("Rejecting event without event ID and timestamp due to already knowing them")
}
return !notOK, err
}
const validateQuery = `
INSERT INTO crypto_message_index (sender_key, session_id, "index", event_id, timestamp)
VALUES ($1, $2, $3, $4, $5)

View file

@ -7,6 +7,8 @@
package ssss
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
@ -108,12 +110,18 @@ func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error)
return nil, err
}
mac, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(data.MAC, "="))
if err != nil {
return nil, err
}
// derive the AES and HMAC keys for the requested event type using the SSSS key
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
// compare the stored MAC with the one we calculated from the ciphertext
calcMac := utils.HMACSHA256B64(payload, hmacKey)
if strings.TrimRight(data.MAC, "=") != calcMac {
h := hmac.New(sha256.New, hmacKey[:])
h.Write(payload)
if !hmac.Equal(h.Sum(nil), mac) {
return nil, ErrKeyDataMACMismatch
}

View file

@ -525,6 +525,9 @@ func (gs *MemoryStore) ValidateMessageIndex(_ context.Context, senderKey id.Send
}
val, ok := gs.MessageIndices[key]
if !ok {
if eventID == "" && timestamp == 0 {
return true, nil
}
gs.MessageIndices[key] = messageIndexValue{
EventID: eventID,
Timestamp: timestamp,

View file

@ -13,6 +13,7 @@ import (
"net/http"
"go.mau.fi/util/exhttp"
"go.mau.fi/util/exmaps"
"golang.org/x/exp/maps"
)
@ -144,10 +145,7 @@ func (e *RespError) UnmarshalJSON(data []byte) error {
}
func (e *RespError) MarshalJSON() ([]byte, error) {
data := maps.Clone(e.ExtraData)
if data == nil {
data = make(map[string]any)
}
data := exmaps.NonNilClone(e.ExtraData)
data["errcode"] = e.ErrCode
data["error"] = e.Err
return json.Marshal(data)
@ -177,6 +175,12 @@ func (e RespError) WithStatus(status int) RespError {
return e
}
func (e RespError) WithExtraData(extraData map[string]any) RespError {
e.ExtraData = exmaps.NonNilClone(e.ExtraData)
maps.Copy(e.ExtraData, extraData)
return e
}
// Error returns the errcode and error message.
func (e RespError) Error() string {
return e.ErrCode + ": " + e.Err

View file

@ -105,3 +105,15 @@ func (bmec *BeeperMuteEventContent) GetMutedUntilTime() time.Time {
}
return time.Time{}
}
func (bmec *BeeperMuteEventContent) GetMuteDuration() time.Duration {
ts := bmec.GetMutedUntilTime()
now := time.Now()
if ts.Before(now) {
return 0
} else if ts == MutedForever {
return -1
} else {
return ts.Sub(now)
}
}

View file

@ -86,6 +86,10 @@ type BeeperRoomKeyAckEventContent struct {
FirstMessageIndex int `json:"first_message_index"`
}
type BeeperChatDeleteEventContent struct {
DeleteForEveryone bool `json:"delete_for_everyone,omitempty"`
}
type IntOrString int
func (ios *IntOrString) UnmarshalJSON(data []byte) error {

View file

@ -55,6 +55,11 @@ export interface RoomFeatures {
allowed_reactions?: string[]
/** Whether custom emoji reactions are allowed. */
custom_emoji_reactions?: boolean
/** Whether deleting the chat for yourself is supported. */
delete_chat?: boolean
/** Whether deleting the chat for all participants is supported. */
delete_chat_for_everyone?: boolean
}
declare type integer = number

View file

@ -18,6 +18,7 @@ import (
"go.mau.fi/util/exerrors"
"go.mau.fi/util/jsontime"
"go.mau.fi/util/ptr"
"golang.org/x/exp/constraints"
"golang.org/x/exp/maps"
)
@ -56,6 +57,7 @@ type RoomFeatures struct {
Archive bool `json:"archive,omitempty"`
MarkAsUnread bool `json:"mark_as_unread,omitempty"`
DeleteChat bool `json:"delete_chat,omitempty"`
DeleteChatForEveryone bool `json:"delete_chat_for_everyone,omitempty"`
}
func (rf *RoomFeatures) GetID() string {
@ -65,10 +67,32 @@ func (rf *RoomFeatures) GetID() string {
return base64.RawURLEncoding.EncodeToString(rf.Hash())
}
func (rf *RoomFeatures) Clone() *RoomFeatures {
if rf == nil {
return nil
}
clone := *rf
clone.File = clone.File.Clone()
clone.Formatting = maps.Clone(clone.Formatting)
clone.EditMaxAge = ptr.Clone(clone.EditMaxAge)
clone.DeleteMaxAge = ptr.Clone(clone.DeleteMaxAge)
clone.DisappearingTimer = clone.DisappearingTimer.Clone()
clone.AllowedReactions = slices.Clone(clone.AllowedReactions)
return &clone
}
type FormattingFeatureMap map[FormattingFeature]CapabilitySupportLevel
type FileFeatureMap map[CapabilityMsgType]*FileFeatures
func (ffm FileFeatureMap) Clone() FileFeatureMap {
dup := maps.Clone(ffm)
for key, value := range dup {
dup[key] = value.Clone()
}
return dup
}
type DisappearingTimerCapability struct {
Types []DisappearingType `json:"types"`
Timers []jsontime.Milliseconds `json:"timers,omitempty"`
@ -76,6 +100,16 @@ type DisappearingTimerCapability struct {
OmitEmptyTimer bool `json:"omit_empty_timer,omitempty"`
}
func (dtc *DisappearingTimerCapability) Clone() *DisappearingTimerCapability {
if dtc == nil {
return nil
}
clone := *dtc
clone.Types = slices.Clone(clone.Types)
clone.Timers = slices.Clone(clone.Timers)
return &clone
}
func (dtc *DisappearingTimerCapability) Supports(content *BeeperDisappearingTimer) bool {
if dtc == nil || content == nil || content.Type == DisappearingTypeNone {
return true
@ -262,6 +296,7 @@ func (rf *RoomFeatures) Hash() []byte {
hashBool(hasher, "archive", rf.Archive)
hashBool(hasher, "mark_as_unread", rf.MarkAsUnread)
hashBool(hasher, "delete_chat", rf.DeleteChat)
hashBool(hasher, "delete_chat_for_everyone", rf.DeleteChatForEveryone)
return hasher.Sum(nil)
}
@ -294,3 +329,13 @@ func (ff *FileFeatures) Hash() []byte {
hashBool(hasher, "view_once", ff.ViewOnce)
return hasher.Sum(nil)
}
func (ff *FileFeatures) Clone() *FileFeatures {
if ff == nil {
return nil
}
clone := *ff
clone.MimeTypes = maps.Clone(clone.MimeTypes)
clone.MaxDuration = ptr.Clone(clone.MaxDuration)
return &clone
}

View file

@ -63,6 +63,7 @@ var TypeMap = map[Type]reflect.Type{
BeeperMessageStatus: reflect.TypeOf(BeeperMessageStatusEventContent{}),
BeeperTranscription: reflect.TypeOf(BeeperTranscriptionEventContent{}),
BeeperDeleteChat: reflect.TypeOf(BeeperChatDeleteEventContent{}),
AccountDataRoomTags: reflect.TypeOf(TagEventContent{}),
AccountDataDirectChats: reflect.TypeOf(DirectChatsEventContent{}),

70
vendor/maunium.net/go/mautrix/event/delayed.go generated vendored Normal file
View file

@ -0,0 +1,70 @@
package event
import (
"encoding/json"
"go.mau.fi/util/jsontime"
"maunium.net/go/mautrix/id"
)
type ScheduledDelayedEvent struct {
DelayID id.DelayID `json:"delay_id"`
RoomID id.RoomID `json:"room_id"`
Type Type `json:"type"`
StateKey *string `json:"state_key,omitempty"`
Delay int64 `json:"delay"`
RunningSince jsontime.UnixMilli `json:"running_since"`
Content Content `json:"content"`
}
func (e ScheduledDelayedEvent) AsEvent(eventID id.EventID, ts jsontime.UnixMilli) (*Event, error) {
evt := &Event{
ID: eventID,
RoomID: e.RoomID,
Type: e.Type,
StateKey: e.StateKey,
Content: e.Content,
Timestamp: ts.UnixMilli(),
}
return evt, evt.Content.ParseRaw(evt.Type)
}
type FinalisedDelayedEvent struct {
DelayedEvent *ScheduledDelayedEvent `json:"scheduled_event"`
Outcome DelayOutcome `json:"outcome"`
Reason DelayReason `json:"reason"`
Error json.RawMessage `json:"error,omitempty"`
EventID id.EventID `json:"event_id,omitempty"`
Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
}
type DelayStatus string
var (
DelayStatusScheduled DelayStatus = "scheduled"
DelayStatusFinalised DelayStatus = "finalised"
)
type DelayAction string
var (
DelayActionSend DelayAction = "send"
DelayActionCancel DelayAction = "cancel"
DelayActionRestart DelayAction = "restart"
)
type DelayOutcome string
var (
DelayOutcomeSend DelayOutcome = "send"
DelayOutcomeCancel DelayOutcome = "cancel"
)
type DelayReason string
var (
DelayReasonAction DelayReason = "action"
DelayReasonError DelayReason = "error"
DelayReasonDelay DelayReason = "delay"
)

View file

@ -35,7 +35,7 @@ type MSC1767Message struct {
}
type PollStartEventContent struct {
RelatesTo *RelatesTo `json:"m.relates_to"`
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
Mentions *Mentions `json:"m.mentions,omitempty"`
PollStart struct {
Kind string `json:"kind"`

View file

@ -231,6 +231,8 @@ type BridgeEventContent struct {
BeeperRoomType string `json:"com.beeper.room_type,omitempty"`
BeeperRoomTypeV2 string `json:"com.beeper.room_type.v2,omitempty"`
TempSlackRemoteIDMigratedFlag bool `json:"com.beeper.slack_remote_id_migrated,omitempty"`
}
// DisappearingType represents the type of a disappearing message timer.

View file

@ -128,7 +128,7 @@ func (et *Type) GuessClass() TypeClass {
InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type,
CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type,
CallNegotiate.Type, CallHangup.Type, BeeperMessageStatus.Type, EventUnstablePollStart.Type, EventUnstablePollResponse.Type,
BeeperTranscription.Type:
EventUnstablePollEnd.Type, BeeperTranscription.Type, BeeperDeleteChat.Type:
return MessageEventType
case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type, ToDeviceRoomKeyWithheld.Type,
ToDeviceBeeperRoomKeyAck.Type:
@ -236,6 +236,7 @@ var (
BeeperMessageStatus = Type{"com.beeper.message_send_status", MessageEventType}
BeeperTranscription = Type{"com.beeper.transcription", MessageEventType}
BeeperDeleteChat = Type{"com.beeper.delete_chat", MessageEventType}
EventUnstablePollStart = Type{Type: "org.matrix.msc3381.poll.start", Class: MessageEventType}
EventUnstablePollResponse = Type{Type: "org.matrix.msc3381.poll.response", Class: MessageEventType}

View file

@ -32,6 +32,9 @@ type EventID string
// https://github.com/matrix-org/matrix-doc/pull/2716
type BatchID string
// A DelayID is a string identifying a delayed event.
type DelayID string
func (roomID RoomID) String() string {
return string(roomID)
}

View file

@ -104,16 +104,24 @@ func ValidateUserLocalpart(localpart string) error {
return nil
}
// ParseAndValidate parses the user ID into the localpart and server name like Parse,
// and also validates that the localpart is allowed according to the user identifiers spec.
func (userID UserID) ParseAndValidate() (localpart, homeserver string, err error) {
localpart, homeserver, err = userID.Parse()
// ParseAndValidateStrict is a stricter version of ParseAndValidateRelaxed that checks the localpart to only allow non-historical localparts.
// This should be used with care: there are real users still using historical localparts.
func (userID UserID) ParseAndValidateStrict() (localpart, homeserver string, err error) {
localpart, homeserver, err = userID.ParseAndValidateRelaxed()
if err == nil {
err = ValidateUserLocalpart(localpart)
}
if err == nil && len(userID) > UserIDMaxLength {
return
}
// ParseAndValidateRelaxed parses the user ID into the localpart and server name like Parse,
// and also validates that the user ID is not too long and that the server name is valid.
func (userID UserID) ParseAndValidateRelaxed() (localpart, homeserver string, err error) {
if len(userID) > UserIDMaxLength {
err = ErrUserIDTooLong
return
}
localpart, homeserver, err = userID.Parse()
if err == nil && !ValidateServerName(homeserver) {
err = fmt.Errorf("%q %q", homeserver, ErrNoncompliantServerPart)
}
@ -121,7 +129,7 @@ func (userID UserID) ParseAndValidate() (localpart, homeserver string, err error
}
func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) {
localpart, homeserver, err = userID.ParseAndValidate()
localpart, homeserver, err = userID.ParseAndValidateStrict()
if err == nil {
localpart, err = DecodeUserLocalpart(localpart)
}

Some files were not shown because too many files have changed in this diff Show more