add room id to !pm mailboxes, fixes #8; update deps

This commit is contained in:
Aine 2025-10-06 00:01:17 +01:00
parent 0fe6c7573d
commit 83d839e561
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
293 changed files with 64080 additions and 38758 deletions

48
go.mod
View file

@ -6,18 +6,18 @@ require (
github.com/archdx/zerolog-sentry v1.8.5 github.com/archdx/zerolog-sentry v1.8.5
github.com/emersion/go-msgauth v0.7.0 github.com/emersion/go-msgauth v0.7.0
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.22.0 github.com/emersion/go-smtp v0.24.0
github.com/etkecc/go-env v1.2.1 github.com/etkecc/go-env v1.2.1
github.com/etkecc/go-fswatcher v1.0.1 github.com/etkecc/go-fswatcher v1.0.1
github.com/etkecc/go-healthchecks/v2 v2.2.2 github.com/etkecc/go-healthchecks/v2 v2.2.2
github.com/etkecc/go-kit v1.7.0 github.com/etkecc/go-kit v1.7.5
github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39 github.com/etkecc/go-linkpearl v0.0.0-20250916123206-78ebb36aa071
github.com/etkecc/go-mxidwc v1.0.1 github.com/etkecc/go-mxidwc v1.0.1
github.com/etkecc/go-secgen v1.4.0 github.com/etkecc/go-secgen v1.4.0
github.com/etkecc/go-validator/v2 v2.2.4 github.com/etkecc/go-validator/v2 v2.2.6
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/gabriel-vasile/mimetype v1.4.9 github.com/gabriel-vasile/mimetype v1.4.10
github.com/getsentry/sentry-go v0.34.0 github.com/getsentry/sentry-go v0.35.3
github.com/jhillyerd/enmime/v2 v2.2.0 github.com/jhillyerd/enmime/v2 v2.2.0
github.com/kvannotten/mailstrip v0.0.0-20200711213611-0002f5c0467e github.com/kvannotten/mailstrip v0.0.0-20200711213611-0002f5c0467e
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
@ -26,9 +26,9 @@ require (
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
maunium.net/go/mautrix v0.24.1 maunium.net/go/mautrix v0.25.1
modernc.org/sqlite v1.38.0 modernc.org/sqlite v1.39.0
) )
require ( require (
@ -39,6 +39,7 @@ require (
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/etkecc/go-trysmtp v1.1.5 // indirect github.com/etkecc/go-trysmtp v1.1.5 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
@ -54,31 +55,32 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/ll v0.1.1 // indirect
github.com/olekukonko/tablewriter v1.0.7 // indirect github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.7.12 // indirect github.com/yuin/goldmark v1.7.13 // indirect
go.mau.fi/util v0.8.8 // indirect go.mau.fi/util v0.9.1 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.44.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.34.0 // indirect golang.org/x/tools v0.37.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.0 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

128
go.sum
View file

@ -16,6 +16,8 @@ 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/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 h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= 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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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/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= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -27,34 +29,34 @@ github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck= github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU= github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.22.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ= github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/etkecc/go-env v1.2.1 h1:b/mIa8D1d9hc3rI8h5bEtBHsnKkBZ6UbmYPog3QIPTU= 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-env v1.2.1/go.mod h1:yTs1DWEsllAZYA417r4V+OmMuYinGXtBzKkLGNvCv5c=
github.com/etkecc/go-fswatcher v1.0.1 h1:n9hqtjzTS3ETb9hcZe1EqYA1lkkhlzZlztu3hXwFDQc= github.com/etkecc/go-fswatcher v1.0.1 h1:n9hqtjzTS3ETb9hcZe1EqYA1lkkhlzZlztu3hXwFDQc=
github.com/etkecc/go-fswatcher v1.0.1/go.mod h1:O5TODJ9z6Qb7X+snqHbB+F0Pah6G497Wdg0SFE/UYpE= github.com/etkecc/go-fswatcher v1.0.1/go.mod h1:O5TODJ9z6Qb7X+snqHbB+F0Pah6G497Wdg0SFE/UYpE=
github.com/etkecc/go-healthchecks/v2 v2.2.2 h1:YV7e+Ga8JY3aZX4Qf6Q1Ca+DnqbT5Drjli3PufB4J1g= github.com/etkecc/go-healthchecks/v2 v2.2.2 h1:YV7e+Ga8JY3aZX4Qf6Q1Ca+DnqbT5Drjli3PufB4J1g=
github.com/etkecc/go-healthchecks/v2 v2.2.2/go.mod h1:IowWGN4F6By6z0eh63+639OscylgtdvT9ITjfN6hnZA= github.com/etkecc/go-healthchecks/v2 v2.2.2/go.mod h1:IowWGN4F6By6z0eh63+639OscylgtdvT9ITjfN6hnZA=
github.com/etkecc/go-kit v1.7.0 h1:jxJ+tWfWwYqgNgfdbZ2va/M0AHyDdKwc8lYGXrFuWsc= github.com/etkecc/go-kit v1.7.5 h1:DbHTbJ69Jnt2d8mjOyKA9f2ARxvi8xgnYgVMlMZd3gI=
github.com/etkecc/go-kit v1.7.0/go.mod h1:yikghi8YaYbTjRXNtx82g0LFv90YqZi2vLf5Chw0ysg= github.com/etkecc/go-kit v1.7.5/go.mod h1:ZFeQrvlMIV6OeZ4XJ090kkyNYRRN4+5wcg1vjnOKEGI=
github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39 h1:wZ+Iv47qoBy3jzzVgzS52KkQWcddUuDsO5s59J2q+1g= github.com/etkecc/go-linkpearl v0.0.0-20250916123206-78ebb36aa071 h1:wcvjEg8togYMrICjibOrmt0ShBKM2vpwO3Tnt3uxq88=
github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39/go.mod h1:v4QqBXgDZ4Hz235SqyaHde1FL+dNpdnjX4tTR4kxcaI= github.com/etkecc/go-linkpearl v0.0.0-20250916123206-78ebb36aa071/go.mod h1:pTOgDOHhro5wOzu1pUYLJMCeyH4CEhijj6tohY5Q2H4=
github.com/etkecc/go-mxidwc v1.0.1 h1:t4Kq3FxSlQjt1i7RpzE5q3cOWjJ0vrTzzGZRSpgh8mg= 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-mxidwc v1.0.1/go.mod h1:WFlntcH4mdual/gNi6X7a6rSJERNuLjdrwM3K/tucQA=
github.com/etkecc/go-secgen v1.4.0 h1:PapLpen3aIqG2LDu+U6KWDb4SMW2UGuvetpJjd86yUc= github.com/etkecc/go-secgen v1.4.0 h1:PapLpen3aIqG2LDu+U6KWDb4SMW2UGuvetpJjd86yUc=
github.com/etkecc/go-secgen v1.4.0/go.mod h1:6dkfdOtmtVxkj1hIJM6U6yA7frSNpr1H5brz5t18Y3I= github.com/etkecc/go-secgen v1.4.0/go.mod h1:6dkfdOtmtVxkj1hIJM6U6yA7frSNpr1H5brz5t18Y3I=
github.com/etkecc/go-trysmtp v1.1.5 h1:MQ4l4bE9nWUqRKoJOalGHGLkb/hRmzbcBMlClufEOng= github.com/etkecc/go-trysmtp v1.1.5 h1:MQ4l4bE9nWUqRKoJOalGHGLkb/hRmzbcBMlClufEOng=
github.com/etkecc/go-trysmtp v1.1.5/go.mod h1:J4zQu+kgM37xQY148wUz0Dm6y+YhtzMghAv/AaXt0sQ= github.com/etkecc/go-trysmtp v1.1.5/go.mod h1:J4zQu+kgM37xQY148wUz0Dm6y+YhtzMghAv/AaXt0sQ=
github.com/etkecc/go-validator/v2 v2.2.4 h1:E4sE4FpYPVP6VRvnndni+8qUXbeSuHxkniFsi6uCRFQ= github.com/etkecc/go-validator/v2 v2.2.6 h1:ybP+zBcXiVf9bvx4BR5OAyjFNYPedYFIN+oGglek/1k=
github.com/etkecc/go-validator/v2 v2.2.4/go.mod h1:5VX2VICgMALoFBiGc02p/6RjQ8ifRxZ0KOuLAH5fTe0= github.com/etkecc/go-validator/v2 v2.2.6/go.mod h1:1jerRLtl69ugBSoGZJJreyzIIYUoHtp3Xj/fmK+12Rc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 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/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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getsentry/sentry-go v0.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4= github.com/getsentry/sentry-go v0.35.3 h1:u5IJaEqZyPdWqe/hKlBKBBnMTSxB/HenCqF3QLabeds=
github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/getsentry/sentry-go v0.35.3/go.mod h1:mdL49ixwT2yi57k5eh7mpnDyPybixPzlzEJFu0Z76QA=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 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-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -109,10 +111,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0= github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0=
github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II= 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 h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
@ -122,14 +124,16 @@ github.com/mileusna/crontab v1.2.0/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
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 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -140,7 +144,6 @@ github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 h1:2by0+lF6NfaNWhl
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39/go.mod h1:idX/fPqwjX31YMTF2iIpEpNApV2YbQhSFr4iIhJaqp4= github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39/go.mod h1:idX/fPqwjX31YMTF2iIpEpNApV2YbQhSFr4iIhJaqp4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@ -153,55 +156,56 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 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/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c= go.mau.fi/util v0.9.1 h1:A+XKHRsjKkFi2qOm4RriR1HqY2hoOXNS3WFHaC89r2Y=
go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c= go.mau.fi/util v0.9.1/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -214,20 +218,20 @@ 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.24.1 h1:09/xi4qTeA03g1n/DPmmqAlT8Cx4QrgwiPlmLVzA9AU= maunium.net/go/mautrix v0.25.1 h1:+xe3eXtQNcDPU/HoWzvSOA5YX57iqlYI1TXf/fM0KWs=
maunium.net/go/mautrix v0.24.1/go.mod h1:Xy6o+pXmbqmgWsUWh15EQ1eozjC+k/VT/7kloByv9PI= maunium.net/go/mautrix v0.25.1/go.mod h1:iSueLJ/2fBaNrsTObGqi1j0cl/loxrtAjmjay1scYD8=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.0.3 h1:y81b9r3asCh6Xtse6Nz85aYGB0cG3M3U6222yap1KWI= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.0.3/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.0 h1:eoFuDb1ozurUY5WSWlgvxHp0FuL+AncMwNjFqGYMJPQ= modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.0/go.mod h1:AiZxInURfEJx516LqEaFcrC+X38rt9G7+8ojIXQKHbo= modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@ -236,8 +240,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 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 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View file

@ -20,6 +20,7 @@ import (
func (b *Bot) sendMailboxes(ctx context.Context) { func (b *Bot) sendMailboxes(ctx context.Context) {
evt := eventFromContext(ctx) evt := eventFromContext(ctx)
mailboxes := map[string]config.Room{} mailboxes := map[string]config.Room{}
rooms := map[string]id.RoomID{}
slice := []string{} slice := []string{}
b.rooms.Range(func(key, value any) bool { b.rooms.Range(func(key, value any) bool {
if key == nil { if key == nil {
@ -43,6 +44,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
} }
mailboxes[mailbox] = cfg mailboxes[mailbox] = cfg
rooms[mailbox] = roomID
slice = append(slice, mailbox) slice = append(slice, mailbox)
return true return true
}) })
@ -61,7 +63,11 @@ func (b *Bot) sendMailboxes(ctx context.Context) {
msg.WriteString(utils.EmailsList(mailbox, cfg.Domain())) msg.WriteString(utils.EmailsList(mailbox, cfg.Domain()))
msg.WriteString("` by ") msg.WriteString("` by ")
msg.WriteString(cfg.Owner()) msg.WriteString(cfg.Owner())
msg.WriteString("\n") msg.WriteString(" in [")
msg.WriteString(rooms[mailbox].String())
msg.WriteString("](https://matrix.to/#/")
msg.WriteString(rooms[mailbox].String())
msg.WriteString(")\n")
} }
b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID))

View file

@ -38,7 +38,7 @@ func NewPlainAuthServer(ctx context.Context, bot matrixbot, conn *smtp.Conn, aut
func (a *PlainAuthServer) Next(response []byte) (challenge []byte, done bool, err error) { func (a *PlainAuthServer) Next(response []byte) (challenge []byte, done bool, err error) {
if a.done { if a.done {
err = sasl.ErrUnexpectedClientResponse err = sasl.ErrUnexpectedClientResponse
return return challenge, done, err
} }
// No initial response, send an empty challenge // No initial response, send an empty challenge
if response == nil { if response == nil {
@ -51,7 +51,7 @@ func (a *PlainAuthServer) Next(response []byte) (challenge []byte, done bool, er
if len(parts) != 3 { if len(parts) != 3 {
a.bot.BanAuth(a.ctx, a.conn.Conn().RemoteAddr()) a.bot.BanAuth(a.ctx, a.conn.Conn().RemoteAddr())
err = errors.New("sasl: invalid response. Don't bother me anymore, kupo") err = errors.New("sasl: invalid response. Don't bother me anymore, kupo")
return return challenge, done, err
} }
identity := string(parts[0]) identity := string(parts[0])
@ -60,5 +60,5 @@ func (a *PlainAuthServer) Next(response []byte) (challenge []byte, done bool, er
err = a.authenticate(identity, username, password) err = a.authenticate(identity, username, password)
done = true done = true
return return challenge, done, err
} }

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

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 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.

View file

@ -0,0 +1,82 @@
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.
## Quick start
```
go get "github.com/clipperhouse/uax29/v2/graphemes"
```
```go
import "github.com/clipperhouse/uax29/v2/graphemes"
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
[![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:
![Go](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
## APIs
### If you have a `string`
```go
text := "Hello, 世界. Nice dog! 👍🐶"
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### If you have an `io.Reader`
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
```go
r := getYourReader() // from a file or network maybe
tokens := graphemes.FromReader(r)
for tokens.Scan() { // Scan() returns true until error or EOF
fmt.Println(tokens.Text()) // Do something with the current grapheme
}
if tokens.Err() != nil { // Check the error
log.Fatal(tokens.Err())
}
```
### If you have a `[]byte`
```go
b := []byte("Hello, 世界. Nice dog! 👍🐶")
tokens := graphemes.FromBytes(b)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### Performance
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.
### Invalid inputs
Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).

View file

@ -0,0 +1,28 @@
package graphemes
import "github.com/clipperhouse/uax29/v2/internal/iterators"
type Iterator[T iterators.Stringish] struct {
*iterators.Iterator[T]
}
var (
splitFuncString = splitFunc[string]
splitFuncBytes = splitFunc[[]byte]
)
// FromString returns an iterator for the grapheme clusters in the input string.
// Iterate while Next() is true, and access the grapheme via Value().
func FromString(s string) Iterator[string] {
return Iterator[string]{
iterators.New(splitFuncString, s),
}
}
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
// Iterate while Next() is true, and access the grapheme via Value().
func FromBytes(b []byte) Iterator[[]byte] {
return Iterator[[]byte]{
iterators.New(splitFuncBytes, b),
}
}

View file

@ -0,0 +1,25 @@
// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
package graphemes
import (
"bufio"
"io"
)
type Scanner struct {
*bufio.Scanner
}
// FromReader returns a Scanner, to split graphemes per
// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
//
// It embeds a [bufio.Scanner], so you can use its methods.
//
// Iterate through graphemes by calling Scan() until false, then check Err().
func FromReader(r io.Reader) *Scanner {
sc := bufio.NewScanner(r)
sc.Split(SplitFunc)
return &Scanner{
Scanner: sc,
}
}

View file

@ -0,0 +1,174 @@
package graphemes
import (
"bufio"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
// is determines if lookup intersects propert(ies)
func (lookup property) is(properties property) bool {
return (lookup & properties) != 0
}
const _Ignore = _Extend
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
//
// 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) {
var empty T
if len(data) == 0 {
return 0, empty, nil
}
// These vars are stateful across loop iterations
var pos int
var lastExIgnore property = 0 // "last excluding ignored categories"
var lastLastExIgnore property = 0 // "last one before that"
var regionalIndicatorCount int
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
current, w := lookup(data[pos:])
if w == 0 {
if !atEOF {
// Rune extends past current data, request more
return 0, empty, nil
}
pos = len(data)
return pos, data[:pos], nil
}
// https://unicode.org/reports/tr29/#GB1
// Start of text always advances
pos += w
for {
eot := pos == len(data) // "end of text"
if eot {
if !atEOF {
// Token extends past current data, request more
return 0, empty, nil
}
// https://unicode.org/reports/tr29/#GB2
break
}
/*
We've switched the evaluation order of GB1 and GB2. It's ok:
because we've checked for len(data) at the top of this function,
sot and eot are mutually exclusive, order doesn't matter.
*/
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
// to the right of the ×, from which we look back or forward
// Remember previous properties to avoid lookups/lookbacks
last := current
if !last.is(_Ignore) {
lastLastExIgnore = lastExIgnore
lastExIgnore = last
}
current, w = lookup(data[pos:])
if w == 0 {
if atEOF {
// Just return the bytes, we can't do anything with them
pos = len(data)
break
}
// Rune extends past current data, request more
return 0, empty, nil
}
// Optimization: no rule can possibly apply
if current|last == 0 { // i.e. both are zero
break
}
// https://unicode.org/reports/tr29/#GB3
if current.is(_LF) && last.is(_CR) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB4
// https://unicode.org/reports/tr29/#GB5
if (current | last).is(_Control | _CR | _LF) {
break
}
// https://unicode.org/reports/tr29/#GB6
if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB7
if current.is(_V|_T) && last.is(_LV|_V) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB8
if current.is(_T) && last.is(_LVT|_T) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9
if current.is(_Extend | _ZWJ) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9a
if current.is(_SpacingMark) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9b
if last.is(_Prepend) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB9c
// TODO(clipperhouse):
// It appears to be added in Unicode 15.1.0:
// https://unicode.org/versions/Unicode15.1.0/#Migration
// This package currently supports Unicode 15.0.0, so
// out of scope for now
// https://unicode.org/reports/tr29/#GB11
if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
pos += w
continue
}
// https://unicode.org/reports/tr29/#GB12
// https://unicode.org/reports/tr29/#GB13
if (current & last).is(_RegionalIndicator) {
regionalIndicatorCount++
odd := regionalIndicatorCount%2 == 1
if odd {
pos += w
continue
}
}
// If we fall through all the above rules, it's a grapheme cluster break
break
}
// Return token
return pos, data[:pos], nil
}

1409
vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,85 @@
package iterators
type Stringish interface {
[]byte | string
}
type SplitFunc[T Stringish] 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 {
split SplitFunc[T]
data T
start int
pos int
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
}
}
// SetText sets the text for the iterator to operate on, and resets all state.
func (iter *Iterator[T]) SetText(data T) {
iter.data = data
iter.start = 0
iter.pos = 0
}
// Split sets the SplitFunc for the Iterator.
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
iter.split = split
}
// Next advances the iterator to the next token. It returns false when there
// are no remaining tokens or an error occurred.
func (iter *Iterator[T]) Next() bool {
if iter.pos == len(iter.data) {
return false
}
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
iter.start = iter.pos
advance, _, err := iter.split(iter.data[iter.pos:], true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
iter.pos += advance
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return true
}
// Value returns the current token.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current token in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current token in the original data.
func (iter *Iterator[T]) End() int {
return iter.pos
}
// Reset resets the iterator to the beginning of the data.
func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}

View file

@ -499,7 +499,7 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
sb.Grow(2048) sb.Grow(2048)
fmt.Fprintf(&sb, "RCPT TO:<%s>", to) fmt.Fprintf(&sb, "RCPT TO:<%s>", to)
if _, ok := c.ext["DSN"]; ok && opts != nil { if _, ok := c.ext["DSN"]; ok && opts != nil {
if opts.Notify != nil && len(opts.Notify) != 0 { if len(opts.Notify) != 0 {
sb.WriteString(" NOTIFY=") sb.WriteString(" NOTIFY=")
if err := checkNotifySet(opts.Notify); err != nil { if err := checkNotifySet(opts.Notify); err != nil {
return errors.New("smtp: Malformed NOTIFY parameter value") return errors.New("smtp: Malformed NOTIFY parameter value")
@ -534,6 +534,22 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
if _, ok := c.ext["RRVS"]; ok && opts != nil && !opts.RequireRecipientValidSince.IsZero() { if _, ok := c.ext["RRVS"]; ok && opts != nil && !opts.RequireRecipientValidSince.IsZero() {
sb.WriteString(fmt.Sprintf(" RRVS=%s", opts.RequireRecipientValidSince.Format(time.RFC3339))) sb.WriteString(fmt.Sprintf(" RRVS=%s", opts.RequireRecipientValidSince.Format(time.RFC3339)))
} }
if _, ok := c.ext["DELIVERBY"]; ok && opts != nil && opts.DeliverBy != nil {
if opts.DeliverBy.Mode == DeliverByReturn && opts.DeliverBy.Time < 1 {
return errors.New("smtp: DELIVERBY mode must be greater than zero with return mode")
}
arg := fmt.Sprintf(" BY=%d;%s", int(opts.DeliverBy.Time.Seconds()), opts.DeliverBy.Mode)
if opts.DeliverBy.Trace {
arg += "T"
}
sb.WriteString(arg)
}
if _, ok := c.ext["MT-PRIORITY"]; ok && opts != nil && opts.MTPriority != nil {
if *opts.MTPriority < -9 || *opts.MTPriority > 9 {
return errors.New("smtp: MT-PRIORITY must be between -9 and 9")
}
sb.WriteString(fmt.Sprintf(" MT-PRIORITY=%d", *opts.MTPriority))
}
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil { if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
return err return err
} }
@ -728,15 +744,6 @@ func (c *Client) SendMail(from string, to []string, r io.Reader) error {
var testHookStartTLS func(*tls.Config) // nil, except for tests var testHookStartTLS func(*tls.Config) // nil, except for tests
func sendMail(addr string, implicitTLS bool, a sasl.Client, from string, to []string, r io.Reader) error { func sendMail(addr string, implicitTLS bool, a sasl.Client, from string, to []string, r io.Reader) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
var ( var (
c *Client c *Client
err error err error

View file

@ -294,6 +294,20 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
if c.server.EnableRRVS { if c.server.EnableRRVS {
caps = append(caps, "RRVS") caps = append(caps, "RRVS")
} }
if c.server.EnableDELIVERBY {
if c.server.MinimumDeliverByTime == 0 {
caps = append(caps, "DELIVERBY")
} else {
caps = append(caps, fmt.Sprintf("DELIVERBY %d", int(c.server.MinimumDeliverByTime.Seconds())))
}
}
if c.server.EnableMTPRIORITY {
if c.server.MtPriorityProfile == PriorityUnspecified {
caps = append(caps, "MT-PRIORITY")
} else {
caps = append(caps, fmt.Sprintf("MT-PRIORITY %s", c.server.MtPriorityProfile))
}
}
args := []string{"Hello " + domain} args := []string{"Hello " + domain}
args = append(args, caps...) args = append(args, caps...)
@ -731,6 +745,38 @@ func (c *Conn) handleRcpt(arg string) {
return return
} }
opts.RequireRecipientValidSince = rrvsTime opts.RequireRecipientValidSince = rrvsTime
case "BY":
if !c.server.EnableDELIVERBY {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "DELIVERBY is not implemented")
return
}
deliverBy := parseDeliverByArgument(value)
if deliverBy == nil {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed BY parameter value")
return
}
if c.server.MinimumDeliverByTime != 0 &&
deliverBy.Mode == DeliverByReturn &&
deliverBy.Time < c.server.MinimumDeliverByTime {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "BY parameter is below server minimum")
return
}
opts.DeliverBy = deliverBy
case "MT-PRIORITY":
if !c.server.EnableMTPRIORITY {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "MT-PRIORITY is not implemented")
return
}
mtPriority, err := strconv.Atoi(value)
if err != nil {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed MT-PRIORITY parameter value")
return
}
if mtPriority < -9 || mtPriority > 9 {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "MT-PRIORITY is outside valid range")
return
}
opts.MTPriority = &mtPriority
default: default:
c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument") c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument")
return return

View file

@ -2,7 +2,9 @@ package smtp
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"time"
) )
// cutPrefixFold is a version of strings.CutPrefix which is case-insensitive. // cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
@ -74,6 +76,29 @@ func parseHelloArgument(arg string) (string, error) {
return domain, nil return domain, nil
} }
// Parses the BY argument defined in RFC2852 section 4.
// Returns pointer to options or nil if invalid.
func parseDeliverByArgument(arg string) *DeliverByOptions {
secondsStr, modeStr, ok := strings.Cut(arg, ";")
if !ok {
return nil
}
modeStr, traceValue := strings.CutSuffix(modeStr, "T")
if modeStr != string(DeliverByNotify) && modeStr != string(DeliverByReturn) {
return nil
}
modeValue := DeliverByMode(modeStr)
secondsValue, err := strconv.Atoi(secondsStr)
if err != nil || (modeValue == DeliverByReturn && secondsValue < 1) {
return nil
}
return &DeliverByOptions{
Time: time.Duration(secondsValue) * time.Second,
Mode: modeValue,
Trace: traceValue,
}
}
// parser parses command arguments defined in RFC 5321 section 4.1.2. // parser parses command arguments defined in RFC 5321 section 4.1.2.
type parser struct { type parser struct {
s string s string

View file

@ -61,6 +61,24 @@ type Server struct {
// Should be used only if backend supports it. // Should be used only if backend supports it.
EnableRRVS bool EnableRRVS bool
// Advertise DELIVERBY (RFC 2852) capability.
// Should be used only if backend supports it.
EnableDELIVERBY bool
// The minimum time, with seconds precision, that a client
// may specify in the BY argument with return mode.
// A zero value indicates no set minimum.
// Only use if DELIVERBY is enabled.
MinimumDeliverByTime time.Duration
// Advertise MT-PRIORITY (RFC 6710) capability.
// Should only be used if backend supports it.
EnableMTPRIORITY bool
// The priority profile mapping as defined
// in RFC 6710 section 10.2.
//
// Default value of NONE to advertise no specific profile.
MtPriorityProfile PriorityProfile
// The server backend. // The server backend.
Backend Backend Backend Backend

View file

@ -3,21 +3,26 @@
// It also implements the following extensions: // It also implements the following extensions:
// //
// - 8BITMIME (RFC 1652) // - 8BITMIME (RFC 1652)
// - AUTH (RFC 2554)
// - STARTTLS (RFC 3207)
// - ENHANCEDSTATUSCODES (RFC 2034) // - ENHANCEDSTATUSCODES (RFC 2034)
// - SMTPUTF8 (RFC 6531) // - AUTH (RFC 2554)
// - REQUIRETLS (RFC 8689) // - DELIVERBY (RFC 2852)
// - CHUNKING (RFC 3030) // - CHUNKING (RFC 3030)
// - BINARYMIME (RFC 3030) // - BINARYMIME (RFC 3030)
// - STARTTLS (RFC 3207)
// - DSN (RFC 3461, RFC 6533) // - DSN (RFC 3461, RFC 6533)
// - SMTPUTF8 (RFC 6531)
// - MT-PRIORITY (RFC 6710)
// - RRVS (RFC 7293)
// - REQUIRETLS (RFC 8689)
// //
// LMTP (RFC 2033) is also supported. // LMTP (RFC 2033) is also supported.
// //
// Additional extensions may be handled by other packages. // Additional extensions may be handled by other packages.
package smtp package smtp
import "time" import (
"time"
)
type BodyType string type BodyType string
@ -84,6 +89,28 @@ const (
DSNAddressTypeUTF8 DSNAddressType = "UTF-8" DSNAddressTypeUTF8 DSNAddressType = "UTF-8"
) )
type DeliverByMode string
const (
DeliverByNotify DeliverByMode = "N"
DeliverByReturn DeliverByMode = "R"
)
type DeliverByOptions struct {
Time time.Duration
Mode DeliverByMode
Trace bool
}
type PriorityProfile string
const (
PriorityUnspecified PriorityProfile = ""
PriorityMIXER PriorityProfile = "MIXER"
PrioritySTANAG4406 PriorityProfile = "STANAG4406"
PriorityNSEP PriorityProfile = "NSEP"
)
// RcptOptions contains parameters for the RCPT command. // RcptOptions contains parameters for the RCPT command.
type RcptOptions struct { type RcptOptions struct {
// Value of NOTIFY= argument, NEVER or a combination of either of // Value of NOTIFY= argument, NEVER or a combination of either of
@ -95,6 +122,12 @@ type RcptOptions struct {
OriginalRecipient string OriginalRecipient string
// Time value of the RRVS= argument // Time value of the RRVS= argument
// Left as the zero time if unset. // or the zero time if unset.
RequireRecipientValidSince time.Time RequireRecipientValidSince time.Time
// Value of BY= argument or nil if unset.
DeliverBy *DeliverByOptions
// Value of MT-PRIORITY= or nil if unset.
MTPriority *int
} }

View file

@ -26,9 +26,18 @@ func AnonymizeIP(ip string) string {
// IPv6 // IPv6
ipParts := strings.Split(parsedIP.String(), ":") ipParts := strings.Split(parsedIP.String(), ":")
if len(ipParts) > 0 { ipParts[len(ipParts)-1] = "0"
ipParts[len(ipParts)-1] = "0" return strings.Join(ipParts, ":")
return strings.Join(ipParts, ":") }
}
return ip // not an ip // IsValidIP checks if the given string is a valid IPv4 or IPv6 address by:
// - ensuring the format is correct
// - ensuring the IP is not an unspecified, loopback, private, multicast, or link-local address
func IsValidIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return !ip.IsUnspecified() && !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsMulticast() && !ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast()
} }

View file

@ -6,6 +6,17 @@ import (
"sort" "sort"
) )
// MapFromSlice creates a map from slice elements as keys
// The map values are set to true, indicating the presence of the key.
// This is useful for quickly checking if a key exists in the map.
func MapFromSlice[T cmp.Ordered](slice []T) map[T]bool {
data := make(map[T]bool, len(slice))
for _, k := range slice {
data[k] = true
}
return data
}
// MapKeys returns map keys only // MapKeys returns map keys only
func MapKeys[T cmp.Ordered, V any](data map[T]V) []T { func MapKeys[T cmp.Ordered, V any](data map[T]V) []T {
keys := make([]T, 0, len(data)) keys := make([]T, 0, len(data))

79
vendor/github.com/etkecc/go-kit/stringsbuilder.go generated vendored Normal file
View file

@ -0,0 +1,79 @@
package kit
import "strings"
// StringsBuilder is a wrapper around strings.Builder with short syntax,
// it also exposes all methods of strings.Builder, so you can use it as a drop-in replacement
type StringsBuilder struct {
b strings.Builder
}
// NewStringsBuilder creates a new StringsBuilder instance
func NewStringsBuilder() StringsBuilder {
return StringsBuilder{}
}
// S is shortcut for WriteString
func (sb *StringsBuilder) S(s string) *StringsBuilder {
sb.b.WriteString(s)
return sb
}
// B is shortcut for WriteByte
func (sb *StringsBuilder) B(b byte) *StringsBuilder {
sb.b.WriteByte(b)
return sb
}
// R is shortcut for WriteRune
func (sb *StringsBuilder) R(r rune) *StringsBuilder {
sb.b.WriteRune(r)
return sb
}
// String returns the accumulated string
func (sb *StringsBuilder) String() string {
return sb.b.String()
}
// Unwrap returns the underlying strings.Builder
func (sb *StringsBuilder) Unwrap() strings.Builder {
return sb.b
}
// WriteString appends the contents of s to sb's buffer
func (sb *StringsBuilder) WriteString(s string) (int, error) {
return sb.b.WriteString(s)
}
// Write appends the contents of p to sb's buffer
func (sb *StringsBuilder) Write(p []byte) (int, error) {
return sb.b.Write(p)
}
// WriteRune appends the UTF-8 encoding of Unicode code point r to sb's buffer
func (sb *StringsBuilder) WriteRune(r rune) (int, error) {
return sb.b.WriteRune(r)
}
// Cap returns the current capacity of the accumulated string
func (sb *StringsBuilder) Cap() int {
return sb.b.Cap()
}
// Grow grows sb's capacity, if necessary, to guarantee space for another n bytes
func (sb *StringsBuilder) Grow(n int) *StringsBuilder {
sb.b.Grow(n)
return sb
}
// Len returns the current length of the accumulated string
func (sb *StringsBuilder) Len() int {
return sb.b.Len()
}
// Reset resets the StringsBuilder to be empty and returns itself
func (sb *StringsBuilder) Reset() *StringsBuilder {
sb.b.Reset()
return sb
}

35
vendor/github.com/etkecc/go-kit/waitgroup.go generated vendored Normal file
View file

@ -0,0 +1,35 @@
package kit
import "sync"
// WaitGroup is a wrapper around sync.WaitGroup to have some syntax sugar
// It does not provide any additional functionality
type WaitGroup struct {
wg *sync.WaitGroup
}
// NewWaitGroup creates a new WaitGroup
func NewWaitGroup() *WaitGroup {
return &WaitGroup{wg: &sync.WaitGroup{}}
}
// Do runs the given functions in separate goroutines and waits for them to complete
func (w *WaitGroup) Do(f ...func()) {
w.wg.Add(len(f))
for _, fn := range f {
go func() {
defer w.wg.Done()
fn()
}()
}
}
// Get returns the underlying sync.WaitGroup
func (w *WaitGroup) Get() *sync.WaitGroup {
return w.wg
}
// Wait waits for all functions to complete
func (w *WaitGroup) Wait() {
w.wg.Wait()
}

View file

@ -1,73 +1,16 @@
version: "2"
run: run:
concurrency: 4 concurrency: 4
timeout: 30m modules-download-mode: readonly
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
build-tags: []
modules-download-mode: readonly
output: output:
formats: formats:
- format: colored-line-number text:
print-issued-lines: true path: stdout
print-linter-name: true print-linter-name: true
sort-results: true print-issued-lines: true
linters-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
nestif:
min-complexity: 5
gocritic:
enabled-tags:
- diagnostic
- style
- performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
extra-rules: true
grouper:
const-require-single-const: true
import-require-single-import: true
var-require-single-var: true
misspell:
locale: US
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
unparam:
check-exported: true
linters: linters:
disable-all: false
enable: enable:
- asasalint - asasalint
- asciicheck - asciicheck
@ -80,7 +23,6 @@ linters:
- dupl - dupl
- dupword - dupword
- durationcheck - durationcheck
- errcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
@ -89,14 +31,8 @@ linters:
- gocognit - gocognit
- gocritic - gocritic
- gocyclo - gocyclo
- gofmt
- gofumpt
- goimports
- gosec - gosec
- gosimple
- gosmopolitan - gosmopolitan
- govet
- ineffassign
- makezero - makezero
- mirror - mirror
- misspell - misspell
@ -106,36 +42,106 @@ linters:
- predeclared - predeclared
- revive - revive
- sqlclosecheck - sqlclosecheck
- staticcheck
- unconvert - unconvert
- unparam - unparam
- unused
- usestdlibvars - usestdlibvars
- wastedassign - wastedassign
fast: false 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
paths:
- mocks
- third_party$
- builtin$
- examples$
issues: issues:
exclude-dirs-use-default: true
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec
- linters:
- staticcheck
text: "SA9003:"
- linters:
- lll
source: "^//go:generate "
- linters:
- revive
text: "returns unexported type"
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
new: false 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$

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

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

View file

@ -1,75 +1,16 @@
version: "2"
run: run:
concurrency: 4 concurrency: 4
timeout: 30m modules-download-mode: readonly
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
build-tags: []
skip-dirs-use-default: true
skip-files: []
modules-download-mode: readonly
output: output:
formats: formats:
- format: colored-line-number text:
print-issued-lines: true path: stdout
print-linter-name: true print-linter-name: true
sort-results: true print-issued-lines: true
linters-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
nestif:
min-complexity: 5
gocritic:
enabled-tags:
- diagnostic
- style
- performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
extra-rules: true
grouper:
const-require-single-const: true
import-require-single-import: true
var-require-single-var: true
misspell:
locale: US
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
unparam:
check-exported: true
linters: linters:
disable-all: false
enable: enable:
- asasalint - asasalint
- asciicheck - asciicheck
@ -82,7 +23,6 @@ linters:
- dupl - dupl
- dupword - dupword
- durationcheck - durationcheck
- errcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
@ -91,14 +31,8 @@ linters:
- gocognit - gocognit
- gocritic - gocritic
- gocyclo - gocyclo
- gofmt
- gofumpt
- goimports
- gosec - gosec
- gosimple
- gosmopolitan - gosmopolitan
- govet
- ineffassign
- makezero - makezero
- mirror - mirror
- misspell - misspell
@ -108,35 +42,109 @@ linters:
- predeclared - predeclared
- revive - revive
- sqlclosecheck - sqlclosecheck
- staticcheck
- unconvert - unconvert
- unparam - unparam
- unused
- usestdlibvars - usestdlibvars
- wastedassign - wastedassign
fast: false 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: issues:
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec
- linters:
- staticcheck
text: "SA9003:"
- linters:
- lll
source: "^//go:generate "
- linters:
- revive
text: "returns unexported type"
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
new: false 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,6 +1,7 @@
package validator package validator
import ( import (
"errors"
"regexp" "regexp"
"strings" "strings"
@ -12,15 +13,22 @@ var domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-
// Domain checks if domain is valid // Domain checks if domain is valid
func (v *V) Domain(domain string) bool { func (v *V) Domain(domain string) bool {
return v.DomainWithError(domain) == nil
}
func (v *V) DomainWithError(domain string) error {
if domain == "" { if domain == "" {
return !v.cfg.Domain.Enforce if v.cfg.Domain.Enforce {
return errors.New("domain is required")
}
return nil
} }
if !v.DomainString(domain) { if !v.DomainString(domain) {
return false return errors.New("domain is invalid")
} }
return true return nil
} }
// DomainString checks if domain string / value is valid using string checks like length and regexp // DomainString checks if domain string / value is valid using string checks like length and regexp

View file

@ -1,6 +1,8 @@
package validator package validator
import ( import (
"context"
"errors"
"fmt" "fmt"
"net" "net"
"net/mail" "net/mail"
@ -9,7 +11,6 @@ import (
"blitiri.com.ar/go/spf" "blitiri.com.ar/go/spf"
"github.com/etkecc/go-trysmtp" "github.com/etkecc/go-trysmtp"
"golang.org/x/net/context"
) )
var ( var (
@ -22,15 +23,23 @@ var (
// Email checks if email is valid // Email checks if email is valid
// returnPath and optionalSenderIP are optional fields // returnPath and optionalSenderIP are optional fields
func (v *V) Email(email, returnPath string, optionalSenderIP ...net.IP) bool { func (v *V) Email(email, returnPath string, optionalSenderIP ...net.IP) bool {
return v.EmailWithError(email, returnPath, optionalSenderIP...) == nil
}
// EmailWithError checks if email is valid and returns an error if not
func (v *V) EmailWithError(email, returnPath string, optionalSenderIP ...net.IP) error {
// edge case: email may be optional // edge case: email may be optional
if email == "" { if email == "" {
return !v.cfg.Email.Enforce if v.cfg.Email.Enforce {
return errors.New("email is required")
}
return nil
} }
address, err := mail.ParseAddress(email) address, err := mail.ParseAddress(email)
if err != nil { if err != nil {
v.cfg.Log("email %s invalid, reason: %v", email, err) v.cfg.Log("email %s invalid, reason: %v", email, err)
return false return err
} }
if returnPath != "" { if returnPath != "" {
rpAddress, err := mail.ParseAddress(returnPath) rpAddress, err := mail.ParseAddress(returnPath)
@ -46,7 +55,7 @@ func (v *V) Email(email, returnPath string, optionalSenderIP ...net.IP) bool {
return v.emailChecks(email, returnPath, optionalSenderIP...) return v.emailChecks(email, returnPath, optionalSenderIP...)
} }
func (v *V) emailChecks(email, returnPath string, optionalSenderIP ...net.IP) bool { func (v *V) emailChecks(email, returnPath string, optionalSenderIP ...net.IP) error {
maxChecks := 4 maxChecks := 4
var senderIP net.IP var senderIP net.IP
if len(optionalSenderIP) > 0 { if len(optionalSenderIP) > 0 {
@ -67,10 +76,10 @@ func (v *V) emailChecks(email, returnPath string, optionalSenderIP ...net.IP) bo
err := <-errchan err := <-errchan
if err != nil { if err != nil {
v.cfg.Log("email %q is invalid, reason: %v", email, err) v.cfg.Log("email %q is invalid, reason: %v", email, err)
return false return err
} }
if checks >= maxChecks { if checks >= maxChecks {
return true return nil
} }
} }
} }

View file

@ -4,7 +4,7 @@ default:
# update go deps # update go deps
update *flags: update *flags:
go get {{flags}} . go get {{ flags }} .
go mod tidy go mod tidy
# run linter # run linter

View file

@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at vasile.gabriel@email.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View file

@ -1,12 +0,0 @@
## Contribute
Contributions to **mimetype** are welcome. If you find an issue and you consider
contributing, you can use the [Github issues tracker](https://github.com/gabriel-vasile/mimetype/issues)
in order to report it, or better yet, open a pull request.
Code contributions must respect these rules:
- code must be test covered
- code must be formatted using gofmt tool
- exported names must be documented
**Important**: By submitting a pull request, you agree to allow the project
owner to license your work under the same license as that used by the project.

View file

@ -27,6 +27,7 @@
- possibility to [extend](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-Extend) with other file formats - possibility to [extend](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-Extend) with other file formats
- common file formats are prioritized - common file formats are prioritized
- [text vs. binary files differentiation](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-TextVsBinary) - [text vs. binary files differentiation](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-TextVsBinary)
- no external dependencies
- safe for concurrent usage - safe for concurrent usage
## Install ## Install
@ -45,8 +46,7 @@ fmt.Println(mtype.String(), mtype.Extension())
``` ```
See the [runnable Go Playground examples](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#pkg-overview). See the [runnable Go Playground examples](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#pkg-overview).
## Usage' Caution: only use libraries like **mimetype** as a last resort. Content type detection
Only use libraries like **mimetype** as a last resort. Content type detection
using magic numbers is slow, inaccurate, and non-standard. Most of the times using magic numbers is slow, inaccurate, and non-standard. Most of the times
protocols have methods for specifying such metadata; e.g., `Content-Type` header protocols have methods for specifying such metadata; e.g., `Content-Type` header
in HTTP and SMTP. in HTTP and SMTP.
@ -67,6 +67,18 @@ mimetype.DetectFile("file.doc")
If increasing the limit does not help, please If increasing the limit does not help, please
[open an issue](https://github.com/gabriel-vasile/mimetype/issues/new?assignees=&labels=&template=mismatched-mime-type-detected.md&title=). [open an issue](https://github.com/gabriel-vasile/mimetype/issues/new?assignees=&labels=&template=mismatched-mime-type-detected.md&title=).
## Tests
In addition to unit tests,
[mimetype_tests](https://github.com/gabriel-vasile/mimetype_tests) compares the
library with the [Unix file utility](https://en.wikipedia.org/wiki/File_(command))
for around 50 000 sample files. Check the latest comparison results
[here](https://github.com/gabriel-vasile/mimetype_tests/actions).
## Benchmarks
Benchmarks for each file format are performed when a PR is open. The results can
be seen on the [workflows page](https://github.com/gabriel-vasile/mimetype/actions/workflows/benchmark.yml).
Performance improvements are welcome but correctness is prioritized.
## Structure ## Structure
**mimetype** uses a hierarchical structure to keep the MIME type detection logic. **mimetype** uses a hierarchical structure to keep the MIME type detection logic.
This reduces the number of calls needed for detecting the file type. The reason This reduces the number of calls needed for detecting the file type. The reason
@ -84,19 +96,8 @@ or from a [file](https://pkg.go.dev/github.com/gabriel-vasile/mimetype#DetectFil
<img alt="how project is structured" src="https://raw.githubusercontent.com/gabriel-vasile/mimetype/master/testdata/gif.gif" width="88%"> <img alt="how project is structured" src="https://raw.githubusercontent.com/gabriel-vasile/mimetype/master/testdata/gif.gif" width="88%">
</div> </div>
## Performance
Thanks to the hierarchical structure, searching for common formats first,
and limiting itself to file headers, **mimetype** matches the performance of
stdlib `http.DetectContentType` while outperforming the alternative package.
```bash
mimetype http.DetectContentType filetype
BenchmarkMatchTar-24 250 ns/op 400 ns/op 3778 ns/op
BenchmarkMatchZip-24 524 ns/op 351 ns/op 4884 ns/op
BenchmarkMatchJpeg-24 103 ns/op 228 ns/op 839 ns/op
BenchmarkMatchGif-24 139 ns/op 202 ns/op 751 ns/op
BenchmarkMatchPng-24 165 ns/op 221 ns/op 1176 ns/op
```
## Contributing ## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). 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.

View file

@ -2,11 +2,10 @@ package charset
import ( import (
"bytes" "bytes"
"encoding/xml"
"strings"
"unicode/utf8" "unicode/utf8"
"golang.org/x/net/html" "github.com/gabriel-vasile/mimetype/internal/markup"
"github.com/gabriel-vasile/mimetype/internal/scan"
) )
const ( const (
@ -141,20 +140,31 @@ func FromXML(content []byte) string {
} }
return FromPlain(content) return FromPlain(content)
} }
func fromXML(content []byte) string { func fromXML(s scan.Bytes) string {
content = trimLWS(content) xml := []byte("<?XML")
dec := xml.NewDecoder(bytes.NewReader(content)) lxml := len(xml)
rawT, err := dec.RawToken() for {
if err != nil { if len(s) == 0 {
return "" return ""
}
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
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
}
aName, aVal, hasMore := "", "", true
for hasMore {
aName, aVal, hasMore = markup.GetAnAttribute(&s)
if aName == "encoding" && aVal != "" {
return aVal
}
}
} }
t, ok := rawT.(xml.ProcInst)
if !ok {
return ""
}
return strings.ToLower(xmlEncoding(string(t.Inst)))
} }
// FromHTML returns the charset of an HTML document. It first looks if a BOM is // FromHTML returns the charset of an HTML document. It first looks if a BOM is
@ -171,139 +181,103 @@ func FromHTML(content []byte) string {
return FromPlain(content) return FromPlain(content)
} }
func fromHTML(content []byte) string { func fromHTML(s scan.Bytes) string {
z := html.NewTokenizer(bytes.NewReader(content)) const (
dontKnow = iota
doNeedPragma
doNotNeedPragma
)
meta := []byte("<META")
body := []byte("<BODY")
lmeta := len(meta)
for { for {
switch z.Next() { if markup.SkipAComment(&s) {
case html.ErrorToken:
return ""
case html.StartTagToken, html.SelfClosingTagToken:
tagName, hasAttr := z.TagName()
if !bytes.Equal(tagName, []byte("meta")) {
continue
}
attrList := make(map[string]bool)
gotPragma := false
const (
dontKnow = iota
doNeedPragma
doNotNeedPragma
)
needPragma := dontKnow
name := ""
for hasAttr {
var key, val []byte
key, val, hasAttr = z.TagAttr()
ks := string(key)
if attrList[ks] {
continue
}
attrList[ks] = true
for i, c := range val {
if 'A' <= c && c <= 'Z' {
val[i] = c + 0x20
}
}
switch ks {
case "http-equiv":
if bytes.Equal(val, []byte("content-type")) {
gotPragma = true
}
case "content":
name = fromMetaElement(string(val))
if name != "" {
needPragma = doNeedPragma
}
case "charset":
name = string(val)
needPragma = doNotNeedPragma
}
}
if needPragma == dontKnow || needPragma == doNeedPragma && !gotPragma {
continue
}
if strings.HasPrefix(name, "utf-16") {
name = "utf-8"
}
return name
}
}
}
func fromMetaElement(s string) string {
for s != "" {
csLoc := strings.Index(s, "charset")
if csLoc == -1 {
return ""
}
s = s[csLoc+len("charset"):]
s = strings.TrimLeft(s, " \t\n\f\r")
if !strings.HasPrefix(s, "=") {
continue continue
} }
s = s[1:] if len(s) <= lmeta {
s = strings.TrimLeft(s, " \t\n\f\r")
if s == "" {
return "" return ""
} }
if q := s[0]; q == '"' || q == '\'' { // Abort when <body is reached.
s = s[1:] if s.Match(body, scan.IgnoreCase) {
closeQuote := strings.IndexRune(s, rune(q)) return ""
if closeQuote == -1 { }
return "" if !s.Match(meta, scan.IgnoreCase) {
s = s[1:] // safe to slice instead of s.Advance(1) because bounds are checked
continue
}
s = s[lmeta:]
c := s.Pop()
if c == 0 || (!scan.ByteIsWS(c) && c != '/') {
return ""
}
attrList := make(map[string]bool)
gotPragma := false
needPragma := dontKnow
charset := ""
aName, aVal, hasMore := "", "", true
for hasMore {
aName, aVal, hasMore = markup.GetAnAttribute(&s)
if attrList[aName] {
continue
}
// processing step
if len(aName) == 0 && len(aVal) == 0 {
if needPragma == dontKnow {
continue
}
if needPragma == doNeedPragma && !gotPragma {
continue
}
}
attrList[aName] = true
if aName == "http-equiv" && scan.Bytes(aVal).Match([]byte("CONTENT-TYPE"), scan.IgnoreCase) {
gotPragma = true
} else if aName == "content" {
charset = string(extractCharsetFromMeta(scan.Bytes(aVal)))
if len(charset) != 0 {
needPragma = doNeedPragma
}
} else if aName == "charset" {
charset = aVal
needPragma = doNotNeedPragma
} }
return s[:closeQuote]
} }
end := strings.IndexAny(s, "; \t\n\f\r") if needPragma == dontKnow || needPragma == doNeedPragma && !gotPragma {
if end == -1 { continue
end = len(s)
} }
return s[:end]
return charset
} }
return ""
} }
func xmlEncoding(s string) string { // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#algorithm-for-extracting-a-character-encoding-from-a-meta-element
param := "encoding=" func extractCharsetFromMeta(s scan.Bytes) []byte {
idx := strings.Index(s, param) for {
if idx == -1 { i := bytes.Index(s, []byte("charset"))
return "" if i == -1 {
} return nil
v := s[idx+len(param):] }
if v == "" { s.Advance(i + len("charset"))
return "" for scan.ByteIsWS(s.Peek()) {
} s.Advance(1)
if v[0] != '\'' && v[0] != '"' { }
return "" if s.Pop() != '=' {
} continue
idx = strings.IndexRune(v[1:], rune(v[0])) }
if idx == -1 { for scan.ByteIsWS(s.Peek()) {
return "" s.Advance(1)
} }
return v[1 : idx+1] quote := s.Peek()
} if quote == 0 {
return nil
}
if quote == '"' || quote == '\'' {
s.Advance(1)
return bytes.TrimSpace(s.PopUntil(quote))
}
// trimLWS trims whitespace from beginning of the input. return bytes.TrimSpace(s.PopUntil(';', '\t', '\n', '\x0c', '\r', ' '))
// TODO: find a way to call trimLWS once per detection instead of once in each
// detector which needs the trimmed input.
func trimLWS(in []byte) []byte {
firstNonWS := 0
for ; firstNonWS < len(in) && isWS(in[firstNonWS]); firstNonWS++ {
} }
return in[firstNonWS:]
}
func isWS(b byte) bool {
return b == '\t' || b == '\n' || b == '\x0c' || b == '\r' || b == ' '
} }

View file

@ -0,0 +1,125 @@
package csv
import (
"bytes"
"github.com/gabriel-vasile/mimetype/internal/scan"
)
// Parser is a CSV reader that only counts fields.
// It avoids allocating/copying memory and to verify behaviour, it is tested
// and fuzzed against encoding/csv parser.
type Parser struct {
comma byte
comment byte
s scan.Bytes
}
func NewParser(comma, comment byte, s scan.Bytes) *Parser {
return &Parser{
comma: comma,
comment: comment,
s: s,
}
}
func (r *Parser) readLine() (line []byte, cutShort bool) {
line = r.s.ReadSlice('\n')
n := len(line)
if n > 0 && line[n-1] == '\r' {
return line[:n-1], false // drop \r at end of line
}
// This line is problematic. The logic from CountFields comes from
// encoding/csv.Reader which relies on mutating the input bytes.
// https://github.com/golang/go/blob/b3251514531123d7fd007682389bce7428d159a0/src/encoding/csv/reader.go#L275-L279
// To avoid mutating the input, we return cutShort. #680
if n >= 2 && line[n-2] == '\r' && line[n-1] == '\n' {
return line[:n-2], true
}
return line, false
}
// CountFields reads one CSV line and counts how many records that line contained.
// hasMore reports whether there are more lines in the input.
// collectIndexes makes CountFields return a list of indexes where CSV fields
// start in the line. These indexes are used to test the correctness against the
// encoding/csv parser.
func (r *Parser) CountFields(collectIndexes bool) (fields int, fieldPos []int, hasMore bool) {
finished := false
var line scan.Bytes
cutShort := false
for {
line, cutShort = r.readLine()
if finished {
return 0, nil, false
}
finished = len(r.s) == 0 && len(line) == 0
if len(line) == lengthNL(line) {
line = nil
continue // Skip empty lines.
}
if len(line) > 0 && line[0] == r.comment {
line = nil
continue
}
break
}
indexes := []int{}
originalLine := line
parseField:
for {
if len(line) == 0 || line[0] != '"' { // non-quoted string field
fields++
if collectIndexes {
indexes = append(indexes, len(originalLine)-len(line))
}
i := bytes.IndexByte(line, r.comma)
if i >= 0 {
line.Advance(i + 1) // 1 to get over ending comma
continue parseField
}
break parseField
} else { // Quoted string field.
if collectIndexes {
indexes = append(indexes, len(originalLine)-len(line))
}
line.Advance(1) // get over starting quote
for {
i := bytes.IndexByte(line, '"')
if i >= 0 {
line.Advance(i + 1) // 1 for ending quote
switch rn := line.Peek(); {
case rn == '"':
line.Advance(1)
case rn == r.comma:
line.Advance(1)
fields++
continue parseField
case lengthNL(line) == len(line):
fields++
break parseField
}
} else if len(line) > 0 || cutShort {
line, cutShort = r.readLine()
originalLine = line
} else {
fields++
break parseField
}
}
}
}
return fields, indexes, fields != 0
}
// lengthNL reports the number of bytes for the trailing \n.
func lengthNL(b []byte) int {
if len(b) > 0 && b[len(b)-1] == '\n' {
return 1
}
return 0
}

View file

@ -258,7 +258,7 @@ out:
} }
func (p *parserState) consumeArray(b []byte, qs []query, lvl int) (n int) { func (p *parserState) consumeArray(b []byte, qs []query, lvl int) (n int) {
p.currPath = append(p.currPath, []byte{'['}) p.appendPath([]byte{'['}, qs)
if len(b) == 0 { if len(b) == 0 {
return 0 return 0
} }
@ -270,7 +270,7 @@ func (p *parserState) consumeArray(b []byte, qs []query, lvl int) (n int) {
} }
if b[n] == ']' { if b[n] == ']' {
p.ib++ p.ib++
p.currPath = p.currPath[:len(p.currPath)-1] p.popLastPath(qs)
return n + 1 return n + 1
} }
innerParsed := p.consumeAny(b[n:], qs, lvl) innerParsed := p.consumeAny(b[n:], qs, lvl)
@ -305,6 +305,20 @@ func queryPathMatch(qs []query, path [][]byte) int {
return -1 return -1
} }
// appendPath will append a path fragment if queries is not empty.
// If we don't need query functionality (just checking if a JSON is valid),
// then we can skip keeping track of the path we're currently in.
func (p *parserState) appendPath(path []byte, qs []query) {
if len(qs) != 0 {
p.currPath = append(p.currPath, path)
}
}
func (p *parserState) popLastPath(qs []query) {
if len(qs) != 0 {
p.currPath = p.currPath[:len(p.currPath)-1]
}
}
func (p *parserState) consumeObject(b []byte, qs []query, lvl int) (n int) { func (p *parserState) consumeObject(b []byte, qs []query, lvl int) (n int) {
for n < len(b) { for n < len(b) {
n += p.consumeSpace(b[n:]) n += p.consumeSpace(b[n:])
@ -326,7 +340,7 @@ func (p *parserState) consumeObject(b []byte, qs []query, lvl int) (n int) {
if keyLen := p.consumeString(b[n:]); keyLen == 0 { if keyLen := p.consumeString(b[n:]); keyLen == 0 {
return 0 return 0
} else { } else {
p.currPath = append(p.currPath, b[n:n+keyLen-1]) p.appendPath(b[n:n+keyLen-1], qs)
if !p.querySatisfied { if !p.querySatisfied {
queryMatched = queryPathMatch(qs, p.currPath) queryMatched = queryPathMatch(qs, p.currPath)
} }
@ -368,12 +382,12 @@ func (p *parserState) consumeObject(b []byte, qs []query, lvl int) (n int) {
} }
switch b[n] { switch b[n] {
case ',': case ',':
p.currPath = p.currPath[:len(p.currPath)-1] p.popLastPath(qs)
n++ n++
p.ib++ p.ib++
continue continue
case '}': case '}':
p.currPath = p.currPath[:len(p.currPath)-1] p.popLastPath(qs)
p.ib++ p.ib++
return n + 1 return n + 1
default: default:
@ -388,6 +402,9 @@ func (p *parserState) consumeAny(b []byte, qs []query, lvl int) (n int) {
if p.maxRecursion != 0 && lvl > p.maxRecursion { if p.maxRecursion != 0 && lvl > p.maxRecursion {
return 0 return 0
} }
if len(qs) == 0 {
p.querySatisfied = true
}
n += p.consumeSpace(b) n += p.consumeSpace(b)
if len(b[n:]) == 0 { if len(b[n:]) == 0 {
return 0 return 0
@ -426,9 +443,6 @@ func (p *parserState) consumeAny(b []byte, qs []query, lvl int) (n int) {
if lvl == 0 { if lvl == 0 {
p.firstToken = t p.firstToken = t
} }
if len(qs) == 0 {
p.querySatisfied = true
}
if rv <= 0 { if rv <= 0 {
return n return n
} }

View file

@ -1,18 +1,11 @@
package magic package magic
import "bytes" import (
"bytes"
"encoding/binary"
)
var ( var (
// Pdf matches a Portable Document Format file.
// https://github.com/file/file/blob/11010cc805546a3e35597e67e1129a481aed40e8/magic/Magdir/pdf
Pdf = prefix(
// usual pdf signature
[]byte("%PDF-"),
// new-line prefixed signature
[]byte("\012%PDF-"),
// UTF-8 BOM prefixed signature
[]byte("\xef\xbb\xbf%PDF-"),
)
// Fdf matches a Forms Data Format file. // Fdf matches a Forms Data Format file.
Fdf = prefix([]byte("%FDF")) Fdf = prefix([]byte("%FDF"))
// Mobi matches a Mobi file. // Mobi matches a Mobi file.
@ -21,8 +14,18 @@ var (
Lit = prefix([]byte("ITOLITLS")) Lit = prefix([]byte("ITOLITLS"))
) )
// PDF matches a Portable Document Format file.
// The %PDF- header should be the first thing inside the file but many
// implementations don't follow the rule. The PDF spec at Appendix H says the
// signature can be prepended by anything.
// https://bugs.astron.com/view.php?id=446
func PDF(raw []byte, _ uint32) bool {
raw = raw[:min(len(raw), 1024)]
return bytes.Contains(raw, []byte("%PDF-"))
}
// DjVu matches a DjVu file. // DjVu matches a DjVu file.
func DjVu(raw []byte, limit uint32) bool { func DjVu(raw []byte, _ uint32) bool {
if len(raw) < 12 { if len(raw) < 12 {
return false return false
} }
@ -36,7 +39,7 @@ func DjVu(raw []byte, limit uint32) bool {
} }
// P7s matches an .p7s signature File (PEM, Base64). // P7s matches an .p7s signature File (PEM, Base64).
func P7s(raw []byte, limit uint32) bool { func P7s(raw []byte, _ uint32) bool {
// Check for PEM Encoding. // Check for PEM Encoding.
if bytes.HasPrefix(raw, []byte("-----BEGIN PKCS7")) { if bytes.HasPrefix(raw, []byte("-----BEGIN PKCS7")) {
return true return true
@ -60,3 +63,21 @@ func P7s(raw []byte, limit uint32) bool {
return false return false
} }
// Lotus123 matches a Lotus 1-2-3 spreadsheet document.
func Lotus123(raw []byte, _ uint32) bool {
if len(raw) <= 20 {
return false
}
version := binary.BigEndian.Uint32(raw)
if version == 0x00000200 {
return raw[6] != 0 && raw[7] == 0
}
return version == 0x00001a00 && raw[20] > 0 && raw[20] < 32
}
// CHM matches a Microsoft Compiled HTML Help file.
func CHM(raw []byte, _ uint32) bool {
return bytes.HasPrefix(raw, []byte("ITSF\003\000\000\000\x60\000\000\000"))
}

View file

@ -4,6 +4,8 @@ package magic
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/gabriel-vasile/mimetype/internal/scan"
) )
type ( type (
@ -74,12 +76,13 @@ func ciCheck(sig, raw []byte) bool {
// matches the raw input. // matches the raw input.
func xml(sigs ...xmlSig) Detector { func xml(sigs ...xmlSig) Detector {
return func(raw []byte, limit uint32) bool { return func(raw []byte, limit uint32) bool {
raw = trimLWS(raw) b := scan.Bytes(raw)
if len(raw) == 0 { b.TrimLWS()
if len(b) == 0 {
return false return false
} }
for _, s := range sigs { for _, s := range sigs {
if xmlCheck(s, raw) { if xmlCheck(s, b) {
return true return true
} }
} }
@ -104,19 +107,19 @@ func xmlCheck(sig xmlSig, raw []byte) bool {
// matches the raw input. // matches the raw input.
func markup(sigs ...[]byte) Detector { func markup(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool { return func(raw []byte, limit uint32) bool {
if bytes.HasPrefix(raw, []byte{0xEF, 0xBB, 0xBF}) { b := scan.Bytes(raw)
if bytes.HasPrefix(b, []byte{0xEF, 0xBB, 0xBF}) {
// We skip the UTF-8 BOM if present to ensure we correctly // We skip the UTF-8 BOM if present to ensure we correctly
// process any leading whitespace. The presence of the BOM // process any leading whitespace. The presence of the BOM
// is taken into account during charset detection in charset.go. // is taken into account during charset detection in charset.go.
raw = trimLWS(raw[3:]) b.Advance(3)
} else {
raw = trimLWS(raw)
} }
if len(raw) == 0 { b.TrimLWS()
if len(b) == 0 {
return false return false
} }
for _, s := range sigs { for _, s := range sigs {
if markupCheck(s, raw) { if markupCheck(s, b) {
return true return true
} }
} }
@ -139,7 +142,7 @@ func markupCheck(sig, raw []byte) bool {
} }
} }
// Next byte must be space or right angle bracket. // Next byte must be space or right angle bracket.
if db := raw[len(sig)]; db != ' ' && db != '>' { if db := raw[len(sig)]; !scan.ByteIsWS(db) && db != '>' {
return false return false
} }
@ -183,8 +186,10 @@ func newXMLSig(localName, xmlns string) xmlSig {
// /usr/bin/env is the interpreter, php is the first and only argument. // /usr/bin/env is the interpreter, php is the first and only argument.
func shebang(sigs ...[]byte) Detector { func shebang(sigs ...[]byte) Detector {
return func(raw []byte, limit uint32) bool { return func(raw []byte, limit uint32) bool {
b := scan.Bytes(raw)
line := b.Line()
for _, s := range sigs { for _, s := range sigs {
if shebangCheck(s, firstLine(raw)) { if shebangCheck(s, line) {
return true return true
} }
} }
@ -192,7 +197,7 @@ func shebang(sigs ...[]byte) Detector {
} }
} }
func shebangCheck(sig, raw []byte) bool { func shebangCheck(sig []byte, raw scan.Bytes) bool {
if len(raw) < len(sig)+2 { if len(raw) < len(sig)+2 {
return false return false
} }
@ -200,52 +205,8 @@ func shebangCheck(sig, raw []byte) bool {
return false return false
} }
return bytes.Equal(trimLWS(trimRWS(raw[2:])), sig) raw.Advance(2) // skip #! we checked above
} raw.TrimLWS()
raw.TrimRWS()
// trimLWS trims whitespace from beginning of the input. return bytes.Equal(raw, sig)
func trimLWS(in []byte) []byte {
firstNonWS := 0
for ; firstNonWS < len(in) && isWS(in[firstNonWS]); firstNonWS++ {
}
return in[firstNonWS:]
}
// trimRWS trims whitespace from the end of the input.
func trimRWS(in []byte) []byte {
lastNonWS := len(in) - 1
for ; lastNonWS > 0 && isWS(in[lastNonWS]); lastNonWS-- {
}
return in[:lastNonWS+1]
}
func firstLine(in []byte) []byte {
lineEnd := 0
for ; lineEnd < len(in) && in[lineEnd] != '\n'; lineEnd++ {
}
return in[:lineEnd]
}
func isWS(b byte) bool {
return b == '\t' || b == '\n' || b == '\x0c' || b == '\r' || b == ' '
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
type readBuf []byte
func (b *readBuf) advance(n int) bool {
if n < 0 || len(*b) < n {
return false
}
*b = (*b)[n:]
return true
} }

View file

@ -7,17 +7,34 @@ import (
// Xlsx matches a Microsoft Excel 2007 file. // Xlsx matches a Microsoft Excel 2007 file.
func Xlsx(raw []byte, limit uint32) bool { func Xlsx(raw []byte, limit uint32) bool {
return zipContains(raw, []byte("xl/"), true) return msoxml(raw, zipEntries{{
name: []byte("xl/"),
dir: true,
}}, 100)
} }
// Docx matches a Microsoft Word 2007 file. // Docx matches a Microsoft Word 2007 file.
func Docx(raw []byte, limit uint32) bool { func Docx(raw []byte, limit uint32) bool {
return zipContains(raw, []byte("word/"), true) return msoxml(raw, zipEntries{{
name: []byte("word/"),
dir: true,
}}, 100)
} }
// Pptx matches a Microsoft PowerPoint 2007 file. // Pptx matches a Microsoft PowerPoint 2007 file.
func Pptx(raw []byte, limit uint32) bool { func Pptx(raw []byte, limit uint32) bool {
return zipContains(raw, []byte("ppt/"), true) return msoxml(raw, zipEntries{{
name: []byte("ppt/"),
dir: true,
}}, 100)
}
// Visio matches a Microsoft Visio 2013+ file.
func Visio(raw []byte, limit uint32) bool {
return msoxml(raw, zipEntries{{
name: []byte("visio/"),
dir: true,
}}, 100)
} }
// Ole matches an Open Linking and Embedding file. // Ole matches an Open Linking and Embedding file.
@ -157,6 +174,14 @@ func Msi(raw []byte, limit uint32) bool {
}) })
} }
// One matches a Microsoft OneNote file.
func One(raw []byte, limit uint32) bool {
return bytes.HasPrefix(raw, []byte{
0xe4, 0x52, 0x5c, 0x7b, 0x8c, 0xd8, 0xa7, 0x4d,
0xae, 0xb1, 0x53, 0x78, 0xd0, 0x29, 0x96, 0xd3,
})
}
// Helper to match by a specific CLSID of a compound file. // Helper to match by a specific CLSID of a compound file.
// //
// http://fileformats.archiveteam.org/wiki/Microsoft_Compound_File // http://fileformats.archiveteam.org/wiki/Microsoft_Compound_File

View file

@ -0,0 +1,111 @@
package magic
import (
"bytes"
"strconv"
"github.com/gabriel-vasile/mimetype/internal/scan"
)
// NetPBM matches a Netpbm Portable BitMap ASCII/Binary file.
//
// See: https://en.wikipedia.org/wiki/Netpbm
func NetPBM(raw []byte, _ uint32) bool {
return netp(raw, "P1\n", "P4\n")
}
// NetPGM matches a Netpbm Portable GrayMap ASCII/Binary file.
//
// See: https://en.wikipedia.org/wiki/Netpbm
func NetPGM(raw []byte, _ uint32) bool {
return netp(raw, "P2\n", "P5\n")
}
// NetPPM matches a Netpbm Portable PixMap ASCII/Binary file.
//
// See: https://en.wikipedia.org/wiki/Netpbm
func NetPPM(raw []byte, _ uint32) bool {
return netp(raw, "P3\n", "P6\n")
}
// NetPAM matches a Netpbm Portable Arbitrary Map file.
//
// See: https://en.wikipedia.org/wiki/Netpbm
func NetPAM(raw []byte, _ uint32) bool {
if !bytes.HasPrefix(raw, []byte("P7\n")) {
return false
}
w, h, d, m, e := false, false, false, false, false
s := scan.Bytes(raw)
var l scan.Bytes
// Read line by line.
for i := 0; i < 128; i++ {
l = s.Line()
// If the line is empty or a comment, skip.
if len(l) == 0 || l.Peek() == '#' {
if len(s) == 0 {
return false
}
continue
} else if bytes.HasPrefix(l, []byte("TUPLTYPE")) {
continue
} else if bytes.HasPrefix(l, []byte("WIDTH ")) {
w = true
} else if bytes.HasPrefix(l, []byte("HEIGHT ")) {
h = true
} else if bytes.HasPrefix(l, []byte("DEPTH ")) {
d = true
} else if bytes.HasPrefix(l, []byte("MAXVAL ")) {
m = true
} else if bytes.HasPrefix(l, []byte("ENDHDR")) {
e = true
}
// When we reached header, return true if we collected all four required headers.
// WIDTH, HEIGHT, DEPTH and MAXVAL.
if e {
return w && h && d && m
}
}
return false
}
func netp(s scan.Bytes, prefixes ...string) bool {
foundPrefix := ""
for _, p := range prefixes {
if bytes.HasPrefix(s, []byte(p)) {
foundPrefix = p
}
}
if foundPrefix == "" {
return false
}
s.Advance(len(foundPrefix)) // jump over P1, P2, P3, etc.
var l scan.Bytes
// Read line by line.
for i := 0; i < 128; i++ {
l = s.Line()
// If the line is a comment, skip.
if l.Peek() == '#' {
continue
}
// If line has leading whitespace, then skip over whitespace.
for scan.ByteIsWS(l.Peek()) {
l.Advance(1)
}
if len(s) == 0 || len(l) > 0 {
break
}
}
// At this point l should be the two integers denoting the size of the matrix.
width := l.PopUntil(scan.ASCIISpaces...)
for scan.ByteIsWS(l.Peek()) {
l.Advance(1)
}
height := l.PopUntil(scan.ASCIISpaces...)
w, errw := strconv.ParseInt(string(width), 10, 64)
h, errh := strconv.ParseInt(string(height), 10, 64)
return errw == nil && errh == nil && w > 0 && h > 0
}

View file

@ -6,6 +6,8 @@ import (
"github.com/gabriel-vasile/mimetype/internal/charset" "github.com/gabriel-vasile/mimetype/internal/charset"
"github.com/gabriel-vasile/mimetype/internal/json" "github.com/gabriel-vasile/mimetype/internal/json"
mkup "github.com/gabriel-vasile/mimetype/internal/markup"
"github.com/gabriel-vasile/mimetype/internal/scan"
) )
var ( var (
@ -27,6 +29,7 @@ var (
[]byte("<BODY"), []byte("<BODY"),
[]byte("<BR"), []byte("<BR"),
[]byte("<P"), []byte("<P"),
[]byte("<!--"),
) )
// XML matches an Extensible Markup Language file. // XML matches an Extensible Markup Language file.
XML = markup([]byte("<?XML")) XML = markup([]byte("<?XML"))
@ -105,6 +108,18 @@ var (
[]byte("/usr/bin/python"), []byte("/usr/bin/python"),
[]byte("/usr/local/bin/python"), []byte("/usr/local/bin/python"),
[]byte("/usr/bin/env python"), []byte("/usr/bin/env python"),
[]byte("/usr/bin/python2"),
[]byte("/usr/local/bin/python2"),
[]byte("/usr/bin/env python2"),
[]byte("/usr/bin/python3"),
[]byte("/usr/local/bin/python3"),
[]byte("/usr/bin/env python3"),
)
// Ruby matches a Ruby programming language file.
Ruby = shebang(
[]byte("/usr/bin/ruby"),
[]byte("/usr/local/bin/ruby"),
[]byte("/usr/bin/env ruby"),
) )
// Tcl matches a Tcl programming language file. // Tcl matches a Tcl programming language file.
Tcl = shebang( Tcl = shebang(
@ -120,19 +135,42 @@ var (
) )
// Rtf matches a Rich Text Format file. // Rtf matches a Rich Text Format file.
Rtf = prefix([]byte("{\\rtf")) Rtf = prefix([]byte("{\\rtf"))
// Shell matches a shell script file.
Shell = shebang(
[]byte("/bin/sh"),
[]byte("/bin/bash"),
[]byte("/usr/local/bin/bash"),
[]byte("/usr/bin/env bash"),
[]byte("/bin/csh"),
[]byte("/usr/local/bin/csh"),
[]byte("/usr/bin/env csh"),
[]byte("/bin/dash"),
[]byte("/usr/local/bin/dash"),
[]byte("/usr/bin/env dash"),
[]byte("/bin/ksh"),
[]byte("/usr/local/bin/ksh"),
[]byte("/usr/bin/env ksh"),
[]byte("/bin/tcsh"),
[]byte("/usr/local/bin/tcsh"),
[]byte("/usr/bin/env tcsh"),
[]byte("/bin/zsh"),
[]byte("/usr/local/bin/zsh"),
[]byte("/usr/bin/env zsh"),
)
) )
// Text matches a plain text file. // Text matches a plain text file.
// //
// TODO: This function does not parse BOM-less UTF16 and UTF32 files. Not really // TODO: This function does not parse BOM-less UTF16 and UTF32 files. Not really
// sure it should. Linux file utility also requires a BOM for UTF16 and UTF32. // sure it should. Linux file utility also requires a BOM for UTF16 and UTF32.
func Text(raw []byte, limit uint32) bool { func Text(raw []byte, _ uint32) bool {
// First look for BOM. // First look for BOM.
if cset := charset.FromBOM(raw); cset != "" { if cset := charset.FromBOM(raw); cset != "" {
return true return true
} }
// Binary data bytes as defined here: https://mimesniff.spec.whatwg.org/#binary-data-byte // Binary data bytes as defined here: https://mimesniff.spec.whatwg.org/#binary-data-byte
for _, b := range raw { for i := 0; i < min(len(raw), 4096); i++ {
b := raw[i]
if b <= 0x08 || if b <= 0x08 ||
b == 0x0B || b == 0x0B ||
0x0E <= b && b <= 0x1A || 0x0E <= b && b <= 0x1A ||
@ -143,6 +181,14 @@ func Text(raw []byte, limit uint32) bool {
return true return true
} }
// 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)]
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
}
// Php matches a PHP: Hypertext Preprocessor file. // Php matches a PHP: Hypertext Preprocessor file.
func Php(raw []byte, limit uint32) bool { func Php(raw []byte, limit uint32) bool {
if res := phpPageF(raw, limit); res { if res := phpPageF(raw, limit); res {
@ -207,10 +253,12 @@ func jsonHelper(raw []byte, limit uint32, q string, wantTok int) bool {
// types. // types.
func NdJSON(raw []byte, limit uint32) bool { func NdJSON(raw []byte, limit uint32) bool {
lCount, objOrArr := 0, 0 lCount, objOrArr := 0, 0
raw = dropLastLine(raw, limit)
var l []byte s := scan.Bytes(raw)
for len(raw) != 0 { s.DropLastLine(limit)
l, raw = scanLine(raw) var l scan.Bytes
for len(s) != 0 {
l = s.Line()
_, inspected, firstToken, _ := json.Parse(json.QueryNone, l) _, inspected, firstToken, _ := json.Parse(json.QueryNone, l)
if len(l) != inspected { if len(l) != inspected {
return false return false
@ -226,18 +274,84 @@ func NdJSON(raw []byte, limit uint32) bool {
// Svg matches a SVG file. // Svg matches a SVG file.
func Svg(raw []byte, limit uint32) bool { func Svg(raw []byte, limit uint32) bool {
return bytes.Contains(raw, []byte("<svg")) return svgWithoutXMLDeclaration(raw) || svgWithXMLDeclaration(raw)
}
// svgWithoutXMLDeclaration matches a SVG image that does not have an XML header.
// Example:
//
// <!-- xml comment ignored -->
// <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
// <rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
// </svg>
func svgWithoutXMLDeclaration(s scan.Bytes) bool {
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
for mkup.SkipAComment(&s) {
}
if !bytes.HasPrefix(s, []byte("<svg")) {
return false
}
targetName, targetVal := "xmlns", "http://www.w3.org/2000/svg"
aName, aVal, hasMore := "", "", true
for hasMore {
aName, aVal, hasMore = mkup.GetAnAttribute(&s)
if aName == targetName && aVal == targetVal {
return true
}
if !hasMore {
return false
}
}
return false
}
// svgWithXMLDeclaration matches a SVG image that has an XML header.
// Example:
//
// <?xml version="1.0" encoding="UTF-8" standalone="no"?>
// <svg width="391" height="391" viewBox="-70.5 -70.5 391 391" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
// <rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
// </svg>
func svgWithXMLDeclaration(s scan.Bytes) bool {
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
if !bytes.HasPrefix(s, []byte("<?xml")) {
return false
}
// version is a required attribute for XML.
hasVersion := false
aName, hasMore := "", true
for hasMore {
aName, _, hasMore = mkup.GetAnAttribute(&s)
if aName == "version" {
hasVersion = true
break
}
if !hasMore {
break
}
}
if len(s) > 4096 {
s = s[:4096]
}
return hasVersion && bytes.Contains(s, []byte("<svg"))
} }
// Srt matches a SubRip file. // Srt matches a SubRip file.
func Srt(raw []byte, _ uint32) bool { func Srt(raw []byte, _ uint32) bool {
line, raw := scanLine(raw) s := scan.Bytes(raw)
line := s.Line()
// First line must be 1. // First line must be 1.
if len(line) != 1 || line[0] != '1' { if len(line) != 1 || line[0] != '1' {
return false return false
} }
line, raw = scanLine(raw) line = s.Line()
// Timestamp format (e.g: 00:02:16,612 --> 00:02:19,376) limits second line // Timestamp format (e.g: 00:02:16,612 --> 00:02:19,376) limits second line
// length to exactly 29 characters. // length to exactly 29 characters.
if len(line) != 29 { if len(line) != 29 {
@ -266,7 +380,7 @@ func Srt(raw []byte, _ uint32) bool {
return false return false
} }
line, _ = scanLine(raw) line = s.Line()
// A third line must exist and not be empty. This is the actual subtitle text. // A third line must exist and not be empty. This is the actual subtitle text.
return len(line) != 0 return len(line) != 0
} }
@ -295,15 +409,3 @@ func Vtt(raw []byte, limit uint32) bool {
return bytes.Equal(raw, []byte{0xEF, 0xBB, 0xBF, 0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) || // UTF-8 BOM and "WEBVTT" return bytes.Equal(raw, []byte{0xEF, 0xBB, 0xBF, 0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) || // UTF-8 BOM and "WEBVTT"
bytes.Equal(raw, []byte{0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) // "WEBVTT" bytes.Equal(raw, []byte{0x57, 0x45, 0x42, 0x56, 0x54, 0x54}) // "WEBVTT"
} }
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
func scanLine(b []byte) (line, remainder []byte) {
line, remainder, _ = bytes.Cut(b, []byte("\n"))
return dropCR(line), remainder
}

View file

@ -1,77 +1,43 @@
package magic package magic
import ( import (
"bufio" "github.com/gabriel-vasile/mimetype/internal/csv"
"bytes" "github.com/gabriel-vasile/mimetype/internal/scan"
"encoding/csv"
"errors"
"io"
"sync"
) )
// A bufio.Reader pool to alleviate problems with memory allocations. // CSV matches a comma-separated values file.
var readerPool = sync.Pool{ func CSV(raw []byte, limit uint32) bool {
New: func() any {
// Initiate with empty source reader.
return bufio.NewReader(nil)
},
}
func newReader(r io.Reader) *bufio.Reader {
br := readerPool.Get().(*bufio.Reader)
br.Reset(r)
return br
}
// Csv matches a comma-separated values file.
func Csv(raw []byte, limit uint32) bool {
return sv(raw, ',', limit) return sv(raw, ',', limit)
} }
// Tsv matches a tab-separated values file. // TSV matches a tab-separated values file.
func Tsv(raw []byte, limit uint32) bool { func TSV(raw []byte, limit uint32) bool {
return sv(raw, '\t', limit) return sv(raw, '\t', limit)
} }
func sv(in []byte, comma rune, limit uint32) bool { func sv(in []byte, comma byte, limit uint32) bool {
in = dropLastLine(in, limit) s := scan.Bytes(in)
s.DropLastLine(limit)
r := csv.NewParser(comma, '#', s)
br := newReader(bytes.NewReader(in)) headerFields, _, hasMore := r.CountFields(false)
defer readerPool.Put(br) if headerFields < 2 || !hasMore {
r := csv.NewReader(br) return false
r.Comma = comma }
r.ReuseRecord = true csvLines := 1 // 1 for header
r.LazyQuotes = true
r.Comment = '#'
lines := 0
for { for {
_, err := r.Read() fields, _, hasMore := r.CountFields(false)
if errors.Is(err, io.EOF) { if !hasMore && fields == 0 {
break break
} }
if err != nil { csvLines++
if fields != headerFields {
return false return false
} }
lines++ if csvLines >= 10 {
} return true
return r.FieldsPerRecord > 1 && lines > 1
}
// dropLastLine drops the last incomplete line from b.
//
// mimetype limits itself to ReadLimit bytes when performing a detection.
// This means, for file formats like CSV for NDJSON, the last line of the input
// can be an incomplete line.
func dropLastLine(b []byte, readLimit uint32) []byte {
if readLimit == 0 || uint32(len(b)) < readLimit {
return b
}
for i := len(b) - 1; i > 0; i-- {
if b[i] == '\n' {
return b[:i]
} }
} }
return b
return csvLines >= 2
} }

View file

@ -2,7 +2,8 @@ package magic
import ( import (
"bytes" "bytes"
"encoding/binary"
"github.com/gabriel-vasile/mimetype/internal/scan"
) )
var ( var (
@ -40,92 +41,149 @@ func Zip(raw []byte, limit uint32) bool {
(raw[3] == 0x4 || raw[3] == 0x6 || raw[3] == 0x8) (raw[3] == 0x4 || raw[3] == 0x6 || raw[3] == 0x8)
} }
// Jar matches a Java archive file. // Jar matches a Java archive file. There are two types of Jar files:
// 1. the ones that can be opened with jexec and have 0xCAFE optional flag
// https://stackoverflow.com/tags/executable-jar/info
// 2. regular jars, same as above, just without the executable flag
// https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=262278#c0
// There is an argument to only check for manifest, since it's the common nominator
// for both executable and non-executable versions. But the traversing zip entries
// is unreliable because it does linear search for signatures
// (instead of relying on offsets told by the file.)
func Jar(raw []byte, limit uint32) bool { func Jar(raw []byte, limit uint32) bool {
return zipContains(raw, []byte("META-INF/MANIFEST.MF"), false) return executableJar(raw) ||
zipHas(raw, zipEntries{{
name: []byte("META-INF/MANIFEST.MF"),
}, {
name: []byte("META-INF/"),
}}, 1)
} }
func zipContains(raw, sig []byte, msoCheck bool) bool { // KMZ matches a zipped KML file, which is "doc.kml" by convention.
b := readBuf(raw) func KMZ(raw []byte, _ uint32) bool {
pk := []byte("PK\003\004") return zipHas(raw, zipEntries{{
if len(b) < 0x1E { name: []byte("doc.kml"),
}}, 100)
}
// An executable Jar has a 0xCAFE flag enabled in the first zip entry.
// The rule from file/file is:
// >(26.s+30) leshort 0xcafe Java archive data (JAR)
func executableJar(b scan.Bytes) bool {
b.Advance(0x1A)
offset, ok := b.Uint16()
if !ok {
return false return false
} }
b.Advance(int(offset) + 2)
if !b.advance(0x1E) { cafe, ok := b.Uint16()
return false return ok && cafe == 0xCAFE
} }
if bytes.HasPrefix(b, sig) {
return true
}
if msoCheck { // zipIterator iterates over a zip file returning the name of the zip entries
skipFiles := [][]byte{ // in that file.
[]byte("[Content_Types].xml"), type zipIterator struct {
[]byte("_rels/.rels"), b scan.Bytes
[]byte("docProps"), }
[]byte("customXml"),
[]byte("[trash]"),
}
hasSkipFile := false type zipEntries []struct {
for _, sf := range skipFiles { name []byte
if bytes.HasPrefix(b, sf) { dir bool // dir means checking just the prefix of the entry, not the whole path
hasSkipFile = true }
break
}
}
if !hasSkipFile {
return false
}
}
searchOffset := binary.LittleEndian.Uint32(raw[18:]) + 49 func (z zipEntries) match(file []byte) bool {
if !b.advance(int(searchOffset)) { for i := range z {
return false if z[i].dir && bytes.HasPrefix(file, z[i].name) {
} return true
nextHeader := bytes.Index(raw[searchOffset:], pk)
if !b.advance(nextHeader) {
return false
}
if bytes.HasPrefix(b, sig) {
return true
}
for i := 0; i < 4; i++ {
if !b.advance(0x1A) {
return false
} }
nextHeader = bytes.Index(b, pk) if bytes.Equal(file, z[i].name) {
if nextHeader == -1 {
return false
}
if !b.advance(nextHeader + 0x1E) {
return false
}
if bytes.HasPrefix(b, sig) {
return true return true
} }
} }
return false return false
} }
func zipHas(raw scan.Bytes, searchFor zipEntries, stopAfter int) bool {
iter := zipIterator{raw}
for i := 0; i < stopAfter; i++ {
f := iter.next()
if len(f) == 0 {
break
}
if searchFor.match(f) {
return true
}
}
return false
}
// msoxml behaves like zipHas, but it puts restrictions on what the first zip
// entry can be.
func msoxml(raw scan.Bytes, searchFor zipEntries, stopAfter int) bool {
iter := zipIterator{raw}
for i := 0; i < stopAfter; i++ {
f := iter.next()
if len(f) == 0 {
break
}
if searchFor.match(f) {
return true
}
// 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]")) {
return false
}
}
}
return false
}
// next extracts the name of the next zip entry.
func (i *zipIterator) next() []byte {
pk := []byte("PK\003\004")
n := bytes.Index(i.b, pk)
if n == -1 {
return nil
}
i.b.Advance(n)
if !i.b.Advance(0x1A) {
return nil
}
l, ok := i.b.Uint16()
if !ok {
return nil
}
if !i.b.Advance(0x02) {
return nil
}
if len(i.b) < int(l) {
return nil
}
return i.b[:l]
}
// APK matches an Android Package Archive. // APK matches an Android Package Archive.
// The source of signatures is https://github.com/file/file/blob/1778642b8ba3d947a779a36fcd81f8e807220a19/magic/Magdir/archive#L1820-L1887 // The source of signatures is https://github.com/file/file/blob/1778642b8ba3d947a779a36fcd81f8e807220a19/magic/Magdir/archive#L1820-L1887
func APK(raw []byte, _ uint32) bool { func APK(raw []byte, _ uint32) bool {
apkSignatures := [][]byte{ return zipHas(raw, zipEntries{{
[]byte("AndroidManifest.xml"), name: []byte("AndroidManifest.xml"),
[]byte("META-INF/com/android/build/gradle/app-metadata.properties"), }, {
[]byte("classes.dex"), name: []byte("META-INF/com/android/build/gradle/app-metadata.properties"),
[]byte("resources.arsc"), }, {
[]byte("res/drawable"), name: []byte("classes.dex"),
} }, {
for _, sig := range apkSignatures { name: []byte("resources.arsc"),
if zipContains(raw, sig, false) { }, {
return true name: []byte("res/drawable"),
} }}, 100)
}
return false
} }

View file

@ -0,0 +1,103 @@
// Package markup implements functions for extracting info from
// HTML and XML documents.
package markup
import (
"bytes"
"github.com/gabriel-vasile/mimetype/internal/scan"
)
func GetAnAttribute(s *scan.Bytes) (name, val string, hasMore bool) {
for scan.ByteIsWS(s.Peek()) || s.Peek() == '/' {
s.Advance(1)
}
if s.Peek() == '>' {
return "", "", 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)
// step 4 and 5
for {
// bap means byte at position in the specification.
bap := s.Pop()
if bap == 0 {
return "", "", false
}
if bap == '=' && len(nameB) > 0 {
val, hasMore := getAValue(s)
return string(nameB), string(val), hasMore
} else if scan.ByteIsWS(bap) {
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
if s.Peek() != '=' {
return string(nameB), "", true
}
s.Advance(1)
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
val, hasMore := getAValue(s)
return string(nameB), string(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)
}
}
}
func getAValue(s *scan.Bytes) (_ []byte, hasMore bool) {
for scan.ByteIsWS(s.Peek()) {
s.Advance(1)
}
origS, end := *s, 0
bap := s.Pop()
if bap == 0 {
return nil, false
}
end++
// Step 10
switch bap {
case '"', '\'':
val := s.PopUntil(bap)
if s.Pop() != bap {
return nil, false
}
return val, s.Peek() != 0 && s.Peek() != '>'
case '>':
return nil, false
}
// Step 11
for {
bap = s.Pop()
if bap == 0 {
return nil, false
}
switch {
case scan.ByteIsWS(bap):
return origS[:end], true
case bap == '>':
return origS[:end], false
default:
end++
}
}
}
func SkipAComment(s *scan.Bytes) (skipped bool) {
if bytes.HasPrefix(*s, []byte("<!--")) {
// Offset by 2 len(<!) because the starting and ending -- can be the same.
if i := bytes.Index((*s)[2:], []byte("-->")); i != -1 {
s.Advance(i + 2 + 3) // 2 comes from len(<!) and 3 comes from len(-->).
return true
}
}
return false
}

View file

@ -0,0 +1,213 @@
// Package scan has functions for scanning byte slices.
package scan
import (
"bytes"
"encoding/binary"
)
// Bytes is a byte slice with helper methods for easier scanning.
type Bytes []byte
func (b *Bytes) Advance(n int) bool {
if n < 0 || len(*b) < n {
return false
}
*b = (*b)[n:]
return true
}
// TrimLWS trims whitespace from beginning of the bytes.
func (b *Bytes) TrimLWS() {
firstNonWS := 0
for ; firstNonWS < len(*b) && ByteIsWS((*b)[firstNonWS]); firstNonWS++ {
}
*b = (*b)[firstNonWS:]
}
// TrimRWS trims whitespace from the end of the bytes.
func (b *Bytes) TrimRWS() {
lb := len(*b)
for lb > 0 && ByteIsWS((*b)[lb-1]) {
*b = (*b)[:lb-1]
lb--
}
}
// Peek one byte from b or 0x00 if b is empty.
func (b *Bytes) Peek() byte {
if len(*b) > 0 {
return (*b)[0]
}
return 0
}
// Pop one byte from b or 0x00 if b is empty.
func (b *Bytes) Pop() byte {
if len(*b) > 0 {
ret := (*b)[0]
*b = (*b)[1:]
return ret
}
return 0
}
// PopN pops n bytes from b or nil if b is empty.
func (b *Bytes) PopN(n int) []byte {
if len(*b) >= n {
ret := (*b)[:n]
*b = (*b)[n:]
return ret
}
return nil
}
// PopUntil will advance b until, but not including, the first occurence of stopAt
// character. If no occurence is found, then it will advance until the end of b.
// The returned Bytes is a slice of all the bytes that we're advanced over.
func (b *Bytes) PopUntil(stopAt ...byte) Bytes {
if len(*b) == 0 {
return Bytes{}
}
i := bytes.IndexAny(*b, string(stopAt))
if i == -1 {
i = len(*b)
}
prefix := (*b)[:i]
*b = (*b)[i:]
return Bytes(prefix)
}
// ReadSlice is the same as PopUntil, but the returned value includes stopAt as well.
func (b *Bytes) ReadSlice(stopAt byte) Bytes {
if len(*b) == 0 {
return Bytes{}
}
i := bytes.IndexByte(*b, stopAt)
if i == -1 {
i = len(*b)
} else {
i++
}
prefix := (*b)[:i]
*b = (*b)[i:]
return Bytes(prefix)
}
// Line returns the first line from b and advances b with the length of the
// line. One new line character is trimmed after the line if it exists.
func (b *Bytes) Line() Bytes {
line := b.PopUntil('\n')
lline := len(line)
if lline > 0 && line[lline-1] == '\r' {
line = line[:lline-1]
}
b.Advance(1)
return line
}
// DropLastLine drops the last incomplete line from b.
//
// mimetype limits itself to ReadLimit bytes when performing a detection.
// This means, for file formats like CSV for NDJSON, the last line of the input
// can be an incomplete line.
// If b length is less than readLimit, it means we received an incomplete file
// and proceed with dropping the last line.
func (b *Bytes) DropLastLine(readLimit uint32) {
if readLimit == 0 || uint32(len(*b)) < readLimit {
return
}
for i := len(*b) - 1; i > 0; i-- {
if (*b)[i] == '\n' {
*b = (*b)[:i]
return
}
}
}
func (b *Bytes) Uint16() (uint16, bool) {
if len(*b) < 2 {
return 0, false
}
v := binary.LittleEndian.Uint16(*b)
*b = (*b)[2:]
return v, true
}
const (
CompactWS = 1 << iota
IgnoreCase
)
// Search for occurences of pattern p inside b at any index.
func (b Bytes) Search(p []byte, flags int) int {
if flags == 0 {
return bytes.Index(b, p)
}
lb, lp := len(b), len(p)
for i := range b {
if lb-i < lp {
return -1
}
if b[i:].Match(p, flags) {
return i
}
}
return 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.
if len(p) == 0 {
return true
}
if flags&IgnoreCase > 0 && isUpper(p[0]) {
if upper(b[0]) != p[0] {
return false
}
b, p = b[1:], p[1:]
} else if flags&CompactWS > 0 && ByteIsWS(p[0]) {
p = p[1:]
if !ByteIsWS(b[0]) {
return false
}
b = b[1:]
if !ByteIsWS(p[0]) {
b.TrimLWS()
}
} else {
if b[0] != p[0] {
return false
}
b, p = b[1:], p[1:]
}
}
return true
}
func isUpper(c byte) bool {
return c >= 'A' && c <= 'Z'
}
func upper(c byte) byte {
if c >= 'a' && c <= 'z' {
return c - ('a' - 'A')
}
return c
}
func ByteIsWS(b byte) bool {
return b == '\t' || b == '\n' || b == '\x0c' || b == '\r' || b == ' '
}
var (
ASCIISpaces = []byte{' ', '\r', '\n', '\x0c', '\t'}
ASCIIDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
)

View file

@ -103,15 +103,17 @@ func (m *MIME) match(in []byte, readLimit uint32) *MIME {
"text/html": charset.FromHTML, "text/html": charset.FromHTML,
"text/xml": charset.FromXML, "text/xml": charset.FromXML,
} }
// ps holds optional MIME parameters. charset := ""
ps := map[string]string{}
if f, ok := needsCharset[m.mime]; ok { if f, ok := needsCharset[m.mime]; ok {
if cset := f(in); cset != "" { // The charset comes from BOM, from HTML headers, from XML headers.
ps["charset"] = cset // Limit the number of bytes searched for to 1024.
} charset = f(in[:min(len(in), 1024)])
}
if m == root {
return m
} }
return m.cloneHierarchy(ps) return m.cloneHierarchy(charset)
} }
// flatten transforms an hierarchy of MIMEs into a slice of MIMEs. // flatten transforms an hierarchy of MIMEs into a slice of MIMEs.
@ -125,10 +127,10 @@ func (m *MIME) flatten() []*MIME {
} }
// clone creates a new MIME with the provided optional MIME parameters. // clone creates a new MIME with the provided optional MIME parameters.
func (m *MIME) clone(ps map[string]string) *MIME { func (m *MIME) clone(charset string) *MIME {
clonedMIME := m.mime clonedMIME := m.mime
if len(ps) > 0 { if charset != "" {
clonedMIME = mime.FormatMediaType(m.mime, ps) clonedMIME = m.mime + "; charset=" + charset
} }
return &MIME{ return &MIME{
@ -140,11 +142,11 @@ func (m *MIME) clone(ps map[string]string) *MIME {
// cloneHierarchy creates a clone of m and all its ancestors. The optional MIME // cloneHierarchy creates a clone of m and all its ancestors. The optional MIME
// parameters are set on the last child of the hierarchy. // parameters are set on the last child of the hierarchy.
func (m *MIME) cloneHierarchy(ps map[string]string) *MIME { func (m *MIME) cloneHierarchy(charset string) *MIME {
ret := m.clone(ps) ret := m.clone(charset)
lastChild := ret lastChild := ret
for p := m.Parent(); p != nil; p = p.Parent() { for p := m.Parent(); p != nil; p = p.Parent() {
pClone := p.clone(nil) pClone := p.clone("")
lastChild.parent = pClone lastChild.parent = pClone
lastChild = pClone lastChild = pClone
} }

View file

@ -1,4 +1,4 @@
## 179 Supported MIME types ## 191 Supported MIME types
This file is automatically generated when running tests. Do not edit manually. This file is automatically generated when running tests. Do not edit manually.
Extension | MIME type | Aliases Extension | MIME type | Aliases
@ -7,12 +7,12 @@ Extension | MIME type | Aliases
**.xpm** | image/x-xpixmap | - **.xpm** | image/x-xpixmap | -
**.7z** | application/x-7z-compressed | - **.7z** | application/x-7z-compressed | -
**.zip** | application/zip | application/x-zip, application/x-zip-compressed **.zip** | application/zip | application/x-zip, application/x-zip-compressed
**.xlsx** | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | -
**.docx** | application/vnd.openxmlformats-officedocument.wordprocessingml.document | - **.docx** | application/vnd.openxmlformats-officedocument.wordprocessingml.document | -
**.pptx** | application/vnd.openxmlformats-officedocument.presentationml.presentation | - **.pptx** | application/vnd.openxmlformats-officedocument.presentationml.presentation | -
**.xlsx** | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | -
**.epub** | application/epub+zip | - **.epub** | application/epub+zip | -
**.apk** | application/vnd.android.package-archive | - **.apk** | application/vnd.android.package-archive | -
**.jar** | application/jar | - **.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 **.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 **.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 **.ods** | application/vnd.oasis.opendocument.spreadsheet | application/x-vnd.oasis.opendocument.spreadsheet
@ -24,6 +24,8 @@ Extension | MIME type | Aliases
**.odf** | application/vnd.oasis.opendocument.formula | application/x-vnd.oasis.opendocument.formula **.odf** | application/vnd.oasis.opendocument.formula | application/x-vnd.oasis.opendocument.formula
**.odc** | application/vnd.oasis.opendocument.chart | application/x-vnd.oasis.opendocument.chart **.odc** | application/vnd.oasis.opendocument.chart | application/x-vnd.oasis.opendocument.chart
**.sxc** | application/vnd.sun.xml.calc | - **.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 **.pdf** | application/pdf | application/x-pdf
**.fdf** | application/vnd.fdf | - **.fdf** | application/vnd.fdf | -
**n/a** | application/x-ole-storage | - **n/a** | application/x-ole-storage | -
@ -61,9 +63,10 @@ Extension | MIME type | Aliases
**.tar** | application/x-tar | - **.tar** | application/x-tar | -
**.xar** | application/x-xar | - **.xar** | application/x-xar | -
**.bz2** | application/x-bzip2 | - **.bz2** | application/x-bzip2 | -
**.fits** | application/fits | - **.fits** | application/fits | image/fits
**.tiff** | image/tiff | - **.tiff** | image/tiff | -
**.bmp** | image/bmp | image/x-bmp, image/x-ms-bmp **.bmp** | image/bmp | image/x-bmp, image/x-ms-bmp
**.123** | application/vnd.lotus-1-2-3 | -
**.ico** | image/x-icon | - **.ico** | image/x-icon | -
**.mp3** | audio/mpeg | audio/x-mpeg, audio/mp3 **.mp3** | audio/mpeg | audio/x-mpeg, audio/mp3
**.flac** | audio/flac | - **.flac** | audio/flac | -
@ -146,9 +149,11 @@ Extension | MIME type | Aliases
**.cab** | application/x-installshield | - **.cab** | application/x-installshield | -
**.jxr** | image/jxr | image/vnd.ms-photo **.jxr** | image/jxr | image/vnd.ms-photo
**.parquet** | application/vnd.apache.parquet | application/x-parquet **.parquet** | application/vnd.apache.parquet | application/x-parquet
**.one** | application/onenote | -
**.chm** | application/vnd.ms-htmlhelp | -
**.txt** | text/plain | - **.txt** | text/plain | -
**.html** | text/html | -
**.svg** | image/svg+xml | - **.svg** | image/svg+xml | -
**.html** | text/html | -
**.xml** | text/xml | application/xml **.xml** | text/xml | application/xml
**.rss** | application/rss+xml | text/rss **.rss** | application/rss+xml | text/rss
**.atom** | application/atom+xml | - **.atom** | application/atom+xml | -
@ -163,11 +168,13 @@ Extension | MIME type | Aliases
**.3mf** | application/vnd.ms-package.3dmanufacturing-3dmodel+xml | - **.3mf** | application/vnd.ms-package.3dmanufacturing-3dmodel+xml | -
**.xfdf** | application/vnd.adobe.xfdf | - **.xfdf** | application/vnd.adobe.xfdf | -
**.owl** | application/owl+xml | - **.owl** | application/owl+xml | -
**.html** | application/xhtml+xml | -
**.php** | text/x-php | - **.php** | text/x-php | -
**.js** | text/javascript | application/x-javascript, application/javascript **.js** | text/javascript | application/x-javascript, application/javascript
**.lua** | text/x-lua | - **.lua** | text/x-lua | -
**.pl** | text/x-perl | - **.pl** | text/x-perl | -
**.py** | text/x-python | text/x-script.python, application/x-python **.py** | text/x-python | text/x-script.python, application/x-python
**.rb** | text/x-ruby | application/x-ruby
**.json** | application/json | - **.json** | application/json | -
**.geojson** | application/geo+json | - **.geojson** | application/geo+json | -
**.har** | application/json | - **.har** | application/json | -
@ -182,3 +189,8 @@ Extension | MIME type | Aliases
**.ics** | text/calendar | - **.ics** | text/calendar | -
**.warc** | application/warc | - **.warc** | application/warc | -
**.vtt** | text/vtt | - **.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 | -

View file

@ -18,12 +18,13 @@ import (
var root = newMIME("application/octet-stream", "", var root = newMIME("application/octet-stream", "",
func([]byte, uint32) bool { return true }, func([]byte, uint32) bool { return true },
xpm, sevenZ, zip, pdf, fdf, ole, ps, psd, p7s, ogg, png, jpg, jxl, jp2, jpx, xpm, sevenZ, zip, pdf, fdf, ole, ps, psd, p7s, ogg, png, jpg, jxl, jp2, jpx,
jpm, jxs, gif, webp, exe, elf, ar, tar, xar, bz2, fits, tiff, bmp, ico, mp3, jpm, jxs, gif, webp, exe, elf, ar, tar, xar, bz2, fits, tiff, bmp, lotus, ico,
flac, midi, ape, musePack, amr, wav, aiff, au, mpeg, quickTime, mp4, webM, mp3, flac, midi, ape, musePack, amr, wav, aiff, au, mpeg, quickTime, mp4, webM,
avi, flv, mkv, asf, aac, voc, m3u, rmvb, gzip, class, swf, crx, ttf, woff, avi, flv, mkv, asf, aac, voc, m3u, rmvb, gzip, class, swf, crx, ttf, woff,
woff2, otf, ttc, eot, wasm, shx, dbf, dcm, rar, djvu, mobi, lit, bpg, cbor, 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, 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, rpm, xz, lzip, torrent, cpio, tzif, xcf, pat, gbr, glb, cabIS, jxr, parquet,
oneNote, chm,
// Keep text last because it is the slowest check. // Keep text last because it is the slowest check.
text, text,
) )
@ -48,22 +49,24 @@ var (
// This means APK should be a child of JAR detector, but in practice, // This means APK should be a child of JAR detector, but in practice,
// the decisive signature for JAR might be located at the end of the file // the decisive signature for JAR might be located at the end of the file
// and not reachable because of library readLimit. // and not reachable because of library readLimit.
zip = newMIME("application/zip", ".zip", magic.Zip, xlsx, docx, pptx, epub, apk, jar, odt, ods, odp, odg, odf, odc, sxc). zip = newMIME("application/zip", ".zip", magic.Zip, docx, pptx, xlsx, epub, apk, jar, odt, ods, odp, odg, odf, odc, sxc, kmz, visio).
alias("application/x-zip", "application/x-zip-compressed") alias("application/x-zip", "application/x-zip-compressed")
tar = newMIME("application/x-tar", ".tar", magic.Tar) tar = newMIME("application/x-tar", ".tar", magic.Tar)
xar = newMIME("application/x-xar", ".xar", magic.Xar) xar = newMIME("application/x-xar", ".xar", magic.Xar)
bz2 = newMIME("application/x-bzip2", ".bz2", magic.Bz2) bz2 = newMIME("application/x-bzip2", ".bz2", magic.Bz2)
pdf = newMIME("application/pdf", ".pdf", magic.Pdf). pdf = newMIME("application/pdf", ".pdf", magic.PDF).
alias("application/x-pdf") alias("application/x-pdf")
fdf = newMIME("application/vnd.fdf", ".fdf", magic.Fdf) fdf = newMIME("application/vnd.fdf", ".fdf", magic.Fdf)
xlsx = newMIME("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", magic.Xlsx) xlsx = newMIME("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", magic.Xlsx)
docx = newMIME("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", magic.Docx) docx = newMIME("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", magic.Docx)
pptx = newMIME("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", magic.Pptx) pptx = newMIME("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", magic.Pptx)
epub = newMIME("application/epub+zip", ".epub", magic.Epub) visio = newMIME("application/vnd.ms-visio.drawing.main+xml", ".vsdx", magic.Visio)
jar = newMIME("application/jar", ".jar", magic.Jar) epub = newMIME("application/epub+zip", ".epub", magic.Epub)
apk = newMIME("application/vnd.android.package-archive", ".apk", magic.APK) jar = newMIME("application/java-archive", ".jar", magic.Jar).
ole = newMIME("application/x-ole-storage", "", magic.Ole, msi, aaf, msg, xls, pub, ppt, doc) alias("application/jar", "application/jar-archive", "application/x-java-archive")
msi = newMIME("application/x-ms-installer", ".msi", magic.Msi). 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)
msi = newMIME("application/x-ms-installer", ".msi", magic.Msi).
alias("application/x-windows-installer", "application/x-msi") alias("application/x-windows-installer", "application/x-msi")
aaf = newMIME("application/octet-stream", ".aaf", magic.Aaf) aaf = newMIME("application/octet-stream", ".aaf", magic.Aaf)
doc = newMIME("application/msword", ".doc", magic.Doc). doc = newMIME("application/msword", ".doc", magic.Doc).
@ -75,18 +78,19 @@ var (
alias("application/msexcel") alias("application/msexcel")
msg = newMIME("application/vnd.ms-outlook", ".msg", magic.Msg) msg = newMIME("application/vnd.ms-outlook", ".msg", magic.Msg)
ps = newMIME("application/postscript", ".ps", magic.Ps) ps = newMIME("application/postscript", ".ps", magic.Ps)
fits = newMIME("application/fits", ".fits", magic.Fits) fits = newMIME("application/fits", ".fits", magic.Fits).alias("image/fits")
ogg = newMIME("application/ogg", ".ogg", magic.Ogg, oggAudio, oggVideo). ogg = newMIME("application/ogg", ".ogg", magic.Ogg, oggAudio, oggVideo).
alias("application/x-ogg") alias("application/x-ogg")
oggAudio = newMIME("audio/ogg", ".oga", magic.OggAudio) oggAudio = newMIME("audio/ogg", ".oga", magic.OggAudio)
oggVideo = newMIME("video/ogg", ".ogv", magic.OggVideo) oggVideo = newMIME("video/ogg", ".ogv", magic.OggVideo)
text = newMIME("text/plain", ".txt", magic.Text, html, svg, xml, php, js, lua, perl, python, json, ndJSON, rtf, srt, tcl, csv, tsv, vCard, iCalendar, warc, vtt) text = newMIME("text/plain", ".txt", magic.Text, svg, html, xml, php, js, lua, perl, python, ruby, json, ndJSON, rtf, srt, tcl, csv, tsv, vCard, iCalendar, warc, vtt, shell, netpbm, netpgm, netppm, netpam)
xml = newMIME("text/xml", ".xml", magic.XML, rss, atom, x3d, kml, xliff, collada, gml, gpx, tcx, amf, threemf, xfdf, owl2). xml = newMIME("text/xml", ".xml", magic.XML, rss, atom, x3d, kml, xliff, collada, gml, gpx, tcx, amf, threemf, xfdf, owl2, xhtml).
alias("application/xml") alias("application/xml")
xhtml = newMIME("application/xhtml+xml", ".html", magic.XHTML)
json = newMIME("application/json", ".json", magic.JSON, geoJSON, har, gltf) json = newMIME("application/json", ".json", magic.JSON, geoJSON, har, gltf)
har = newMIME("application/json", ".har", magic.HAR) har = newMIME("application/json", ".har", magic.HAR)
csv = newMIME("text/csv", ".csv", magic.Csv) csv = newMIME("text/csv", ".csv", magic.CSV)
tsv = newMIME("text/tab-separated-values", ".tsv", magic.Tsv) tsv = newMIME("text/tab-separated-values", ".tsv", magic.TSV)
geoJSON = newMIME("application/geo+json", ".geojson", magic.GeoJSON) geoJSON = newMIME("application/geo+json", ".geojson", magic.GeoJSON)
ndJSON = newMIME("application/x-ndjson", ".ndjson", magic.NdJSON) ndJSON = newMIME("application/x-ndjson", ".ndjson", magic.NdJSON)
html = newMIME("text/html", ".html", magic.HTML) html = newMIME("text/html", ".html", magic.HTML)
@ -101,6 +105,10 @@ var (
perl = newMIME("text/x-perl", ".pl", magic.Perl) perl = newMIME("text/x-perl", ".pl", magic.Perl)
python = newMIME("text/x-python", ".py", magic.Python). python = newMIME("text/x-python", ".py", magic.Python).
alias("text/x-script.python", "application/x-python") alias("text/x-script.python", "application/x-python")
ruby = newMIME("text/x-ruby", ".rb", magic.Ruby).
alias("application/x-ruby")
shell = newMIME("text/x-shellscript", ".sh", magic.Shell).
alias("text/x-sh", "application/x-shellscript", "application/x-sh")
tcl = newMIME("text/x-tcl", ".tcl", magic.Tcl). tcl = newMIME("text/x-tcl", ".tcl", magic.Tcl).
alias("application/x-tcl") alias("application/x-tcl")
vCard = newMIME("text/vcard", ".vcf", magic.VCard) vCard = newMIME("text/vcard", ".vcf", magic.VCard)
@ -112,6 +120,7 @@ var (
atom = newMIME("application/atom+xml", ".atom", magic.Atom) atom = newMIME("application/atom+xml", ".atom", magic.Atom)
x3d = newMIME("model/x3d+xml", ".x3d", magic.X3d) x3d = newMIME("model/x3d+xml", ".x3d", magic.X3d)
kml = newMIME("application/vnd.google-earth.kml+xml", ".kml", magic.Kml) kml = newMIME("application/vnd.google-earth.kml+xml", ".kml", magic.Kml)
kmz = newMIME("application/vnd.google-earth.kmz", ".kmz", magic.KMZ)
xliff = newMIME("application/x-xliff+xml", ".xlf", magic.Xliff) xliff = newMIME("application/x-xliff+xml", ".xlf", magic.Xliff)
collada = newMIME("model/vnd.collada+xml", ".dae", magic.Collada) collada = newMIME("model/vnd.collada+xml", ".dae", magic.Collada)
gml = newMIME("application/gml+xml", ".gml", magic.Gml) gml = newMIME("application/gml+xml", ".gml", magic.Gml)
@ -135,9 +144,12 @@ var (
tiff = newMIME("image/tiff", ".tiff", magic.Tiff) tiff = newMIME("image/tiff", ".tiff", magic.Tiff)
bmp = newMIME("image/bmp", ".bmp", magic.Bmp). bmp = newMIME("image/bmp", ".bmp", magic.Bmp).
alias("image/x-bmp", "image/x-ms-bmp") alias("image/x-bmp", "image/x-ms-bmp")
ico = newMIME("image/x-icon", ".ico", magic.Ico) // lotus check must be done before ico because some ico detection is a bit
icns = newMIME("image/x-icns", ".icns", magic.Icns) // relaxed and some lotus files are wrongfully identified as ico otherwise.
psd = newMIME("image/vnd.adobe.photoshop", ".psd", magic.Psd). lotus = newMIME("application/vnd.lotus-1-2-3", ".123", magic.Lotus123)
ico = newMIME("image/x-icon", ".ico", magic.Ico)
icns = newMIME("image/x-icns", ".icns", magic.Icns)
psd = newMIME("image/vnd.adobe.photoshop", ".psd", magic.Psd).
alias("image/x-psd", "application/photoshop") alias("image/x-psd", "application/photoshop")
heic = newMIME("image/heic", ".heic", magic.Heic) heic = newMIME("image/heic", ".heic", magic.Heic)
heicSeq = newMIME("image/heic-sequence", ".heic", magic.HeicSequence) heicSeq = newMIME("image/heic-sequence", ".heic", magic.HeicSequence)
@ -267,5 +279,11 @@ var (
jxr = newMIME("image/jxr", ".jxr", magic.Jxr).alias("image/vnd.ms-photo") jxr = newMIME("image/jxr", ".jxr", magic.Jxr).alias("image/vnd.ms-photo")
parquet = newMIME("application/vnd.apache.parquet", ".parquet", magic.Par1). parquet = newMIME("application/vnd.apache.parquet", ".parquet", magic.Par1).
alias("application/x-parquet") alias("application/x-parquet")
cbor = newMIME("application/cbor", ".cbor", magic.CBOR) netpbm = newMIME("image/x-portable-bitmap", ".pbm", magic.NetPBM)
netpgm = newMIME("image/x-portable-graymap", ".pgm", magic.NetPGM)
netppm = newMIME("image/x-portable-pixmap", ".ppm", magic.NetPPM)
netpam = newMIME("image/x-portable-arbitrarymap", ".pam", magic.NetPAM)
cbor = newMIME("application/cbor", ".cbor", magic.CBOR)
oneNote = newMIME("application/onenote", ".one", magic.One)
chm = newMIME("application/vnd.ms-htmlhelp", ".chm", magic.CHM)
) )

View file

@ -1,5 +1,88 @@
# Changelog # Changelog
## 0.35.3
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.3.
### Bug Fixes
- Add missing rate limit categories ([#1082](https://github.com/getsentry/sentry-go/pull/1082))
## 0.35.2
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.2.
### Bug Fixes
- Fix OpenTelemetry spans being created as transactions instead of child spans ([#1073](https://github.com/getsentry/sentry-go/pull/1073))
### Misc
- Add `MockTransport` to test clients for improved testing ([#1071](https://github.com/getsentry/sentry-go/pull/1071))
## 0.35.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.1.
### Bug Fixes
- Fix race conditions when accessing the scope during logging operations ([#1050](https://github.com/getsentry/sentry-go/pull/1050))
- Fix nil pointer dereference with malformed URLs when tracing is enabled in `fasthttp` and `fiber` integrations ([#1055](https://github.com/getsentry/sentry-go/pull/1055))
### Misc
- Bump `github.com/gofiber/fiber/v2` from 2.52.5 to 2.52.9 in `/fiber` ([#1067](https://github.com/getsentry/sentry-go/pull/1067))
## 0.35.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.0.
### Breaking Changes
- Changes to the logging API ([#1046](https://github.com/getsentry/sentry-go/pull/1046))
The logging API now supports a fluent interface for structured logging with attributes:
```go
// usage before
logger := sentry.NewLogger(ctx)
// attributes weren't being set permanently
logger.SetAttributes(
attribute.String("version", "1.0.0"),
)
logger.Infof(ctx, "Message with parameters %d and %d", 1, 2)
// new behavior
ctx := context.Background()
logger := sentry.NewLogger(ctx)
// Set permanent attributes on the logger
logger.SetAttributes(
attribute.String("version", "1.0.0"),
)
// Chain attributes on individual log entries
logger.Info().
String("key.string", "value").
Int("key.int", 42).
Bool("key.bool", true).
Emitf("Message with parameters %d and %d", 1, 2)
```
### Bug Fixes
- Correctly serialize `FailureIssueThreshold` and `RecoveryThreshold` onto check-in payloads ([#1060](https://github.com/getsentry/sentry-go/pull/1060))
## 0.34.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.1.
### Bug Fixes
- Allow flush to be used multiple times without issues, particularly for the batch logger ([#1051](https://github.com/getsentry/sentry-go/pull/1051))
- Fix race condition in `Scope.GetSpan()` method by adding proper mutex locking ([#1044](https://github.com/getsentry/sentry-go/pull/1044))
- Guard transport on `Close()` to prevent panic when called multiple times ([#1044](https://github.com/getsentry/sentry-go/pull/1044))
## 0.34.0 ## 0.34.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.0. The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.0.

View file

@ -54,12 +54,23 @@ test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage en
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \ $(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
done; done;
.PHONY: test-coverage clean-report-dir .PHONY: test-coverage clean-report-dir
test-race-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Run tests with race detection and coverage
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
echo ">>> Running tests with race detection and coverage for module: $${dir}"; \
DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \
REPORT_NAME=$$(basename $${DIR_ABS}); \
(cd "$${dir}" && \
$(GO) test -count=1 -timeout $(TIMEOUT)s -race -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \
cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
done;
.PHONY: test-race-coverage
mod-tidy: ## Check go.mod tidiness mod-tidy: ## Check go.mod tidiness
set -e ; \ set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \ for dir in $(ALL_GO_MOD_DIRS); do \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \ echo ">>> Running 'go mod tidy' for module: $${dir}"; \
(cd "$${dir}" && go mod tidy -go=1.21 -compat=1.21); \ (cd "$${dir}" && go mod tidy -go=1.22 -compat=1.22); \
done; \ done; \
git diff --exit-code; git diff --exit-code;
.PHONY: mod-tidy .PHONY: mod-tidy

View file

@ -12,17 +12,20 @@ const (
) )
type BatchLogger struct { type BatchLogger struct {
client *Client client *Client
logCh chan Log logCh chan Log
cancel context.CancelFunc flushCh chan chan struct{}
wg sync.WaitGroup cancel context.CancelFunc
startOnce sync.Once wg sync.WaitGroup
startOnce sync.Once
shutdownOnce sync.Once
} }
func NewBatchLogger(client *Client) *BatchLogger { func NewBatchLogger(client *Client) *BatchLogger {
return &BatchLogger{ return &BatchLogger{
client: client, client: client,
logCh: make(chan Log, batchSize), logCh: make(chan Log, batchSize),
flushCh: make(chan chan struct{}),
} }
} }
@ -35,17 +38,32 @@ func (l *BatchLogger) Start() {
}) })
} }
func (l *BatchLogger) Flush() { func (l *BatchLogger) Flush(timeout <-chan struct{}) {
if l.cancel != nil { done := make(chan struct{})
l.cancel() select {
l.wg.Wait() case l.flushCh <- done:
select {
case <-done:
case <-timeout:
}
case <-timeout:
} }
} }
func (l *BatchLogger) Shutdown() {
l.shutdownOnce.Do(func() {
if l.cancel != nil {
l.cancel()
l.wg.Wait()
}
})
}
func (l *BatchLogger) run(ctx context.Context) { func (l *BatchLogger) run(ctx context.Context) {
defer l.wg.Done() defer l.wg.Done()
var logs []Log var logs []Log
timer := time.NewTimer(batchTimeout) timer := time.NewTimer(batchTimeout)
defer timer.Stop()
for { for {
select { select {
@ -65,8 +83,27 @@ func (l *BatchLogger) run(ctx context.Context) {
logs = nil logs = nil
} }
timer.Reset(batchTimeout) timer.Reset(batchTimeout)
case done := <-l.flushCh:
flushDrain:
for {
select {
case log := <-l.logCh:
logs = append(logs, log)
default:
break flushDrain
}
}
if len(logs) > 0 {
l.processEvent(logs)
logs = nil
}
if !timer.Stop() {
<-timer.C
}
timer.Reset(batchTimeout)
close(done)
case <-ctx.Done(): case <-ctx.Done():
// Drain remaining logs from channel
drain: drain:
for { for {
select { select {

View file

@ -511,7 +511,9 @@ func (client *Client) RecoverWithContext(
// call to Init. // call to Init.
func (client *Client) Flush(timeout time.Duration) bool { func (client *Client) Flush(timeout time.Duration) bool {
if client.batchLogger != nil { if client.batchLogger != nil {
client.batchLogger.Flush() ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return client.FlushWithContext(ctx)
} }
return client.Transport.Flush(timeout) return client.Transport.Flush(timeout)
} }
@ -530,7 +532,7 @@ func (client *Client) Flush(timeout time.Duration) bool {
func (client *Client) FlushWithContext(ctx context.Context) bool { func (client *Client) FlushWithContext(ctx context.Context) bool {
if client.batchLogger != nil { if client.batchLogger != nil {
client.batchLogger.Flush() client.batchLogger.Flush(ctx.Done())
} }
return client.Transport.FlushWithContext(ctx) return client.Transport.FlushWithContext(ctx)
} }

View file

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/attribute"
"github.com/getsentry/sentry-go/internal/ratelimit"
) )
const eventType = "event" const eventType = "event"
@ -101,6 +102,7 @@ func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
return json.Marshal((*breadcrumb)(b)) return json.Marshal((*breadcrumb)(b))
} }
// Logger provides a chaining API for structured logging to Sentry.
type Logger interface { type Logger interface {
// Write implements the io.Writer interface. Currently, the [sentry.Hub] is // Write implements the io.Writer interface. Currently, the [sentry.Hub] is
// context aware, in order to get the correct trace correlation. Using this // context aware, in order to get the correct trace correlation. Using this
@ -108,51 +110,47 @@ type Logger interface {
// Write it is recommended to create a NewLogger so that the associated context // Write it is recommended to create a NewLogger so that the associated context
// is passed correctly. // is passed correctly.
Write(p []byte) (n int, err error) Write(p []byte) (n int, err error)
// Trace emits a [LogLevelTrace] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Trace(ctx context.Context, v ...interface{})
// Debug emits a [LogLevelDebug] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Debug(ctx context.Context, v ...interface{})
// Info emits a [LogLevelInfo] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Info(ctx context.Context, v ...interface{})
// Warn emits a [LogLevelWarn] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Warn(ctx context.Context, v ...interface{})
// Error emits a [LogLevelError] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Error(ctx context.Context, v ...interface{})
// Fatal emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
// Arguments are handled in the manner of [fmt.Print].
Fatal(ctx context.Context, v ...interface{})
// Panic emits a [LogLevelFatal] log to Sentry followed by a call to panic().
// Arguments are handled in the manner of [fmt.Print].
Panic(ctx context.Context, v ...interface{})
// Tracef emits a [LogLevelTrace] log to Sentry. // SetAttributes allows attaching parameters to the logger using the attribute API.
// Arguments are handled in the manner of [fmt.Printf]. // These attributes will be included in all subsequent log entries.
Tracef(ctx context.Context, format string, v ...interface{})
// Debugf emits a [LogLevelDebug] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Debugf(ctx context.Context, format string, v ...interface{})
// Infof emits a [LogLevelInfo] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Infof(ctx context.Context, format string, v ...interface{})
// Warnf emits a [LogLevelWarn] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Warnf(ctx context.Context, format string, v ...interface{})
// Errorf emits a [LogLevelError] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Errorf(ctx context.Context, format string, v ...interface{})
// Fatalf emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
// Arguments are handled in the manner of [fmt.Printf].
Fatalf(ctx context.Context, format string, v ...interface{})
// Panicf emits a [LogLevelFatal] log to Sentry followed by a call to panic().
// Arguments are handled in the manner of [fmt.Printf].
Panicf(ctx context.Context, format string, v ...interface{})
// SetAttributes allows attaching parameters to the log message using the attribute API.
SetAttributes(...attribute.Builder) SetAttributes(...attribute.Builder)
// Trace defines the [sentry.LogLevel] for the log entry.
Trace() LogEntry
// Debug defines the [sentry.LogLevel] for the log entry.
Debug() LogEntry
// Info defines the [sentry.LogLevel] for the log entry.
Info() LogEntry
// Warn defines the [sentry.LogLevel] for the log entry.
Warn() LogEntry
// Error defines the [sentry.LogLevel] for the log entry.
Error() LogEntry
// Fatal defines the [sentry.LogLevel] for the log entry.
Fatal() LogEntry
// Panic defines the [sentry.LogLevel] for the log entry.
Panic() LogEntry
// GetCtx returns the [context.Context] set on the logger.
GetCtx() context.Context
}
// LogEntry defines the interface for a log entry that supports chaining attributes.
type LogEntry interface {
// WithCtx creates a new LogEntry with the specified context without overwriting the previous one.
WithCtx(ctx context.Context) LogEntry
// String adds a string attribute to the LogEntry.
String(key, value string) LogEntry
// Int adds an int attribute to the LogEntry.
Int(key string, value int) LogEntry
// Int64 adds an int64 attribute to the LogEntry.
Int64(key string, value int64) LogEntry
// Float64 adds a float64 attribute to the LogEntry.
Float64(key string, value float64) LogEntry
// Bool adds a bool attribute to the LogEntry.
Bool(key string, value bool) LogEntry
// Emit emits the LogEntry with the provided arguments.
Emit(args ...interface{})
// Emitf emits the LogEntry using a format string and arguments.
Emitf(format string, args ...interface{})
} }
// Attachment allows associating files with your events to aid in investigation. // Attachment allows associating files with your events to aid in investigation.
@ -595,16 +593,33 @@ func (e *Event) checkInMarshalJSON() ([]byte, error) {
if e.MonitorConfig != nil { if e.MonitorConfig != nil {
checkIn.MonitorConfig = &MonitorConfig{ checkIn.MonitorConfig = &MonitorConfig{
Schedule: e.MonitorConfig.Schedule, Schedule: e.MonitorConfig.Schedule,
CheckInMargin: e.MonitorConfig.CheckInMargin, CheckInMargin: e.MonitorConfig.CheckInMargin,
MaxRuntime: e.MonitorConfig.MaxRuntime, MaxRuntime: e.MonitorConfig.MaxRuntime,
Timezone: e.MonitorConfig.Timezone, Timezone: e.MonitorConfig.Timezone,
FailureIssueThreshold: e.MonitorConfig.FailureIssueThreshold,
RecoveryThreshold: e.MonitorConfig.RecoveryThreshold,
} }
} }
return json.Marshal(checkIn) return json.Marshal(checkIn)
} }
func (e *Event) toCategory() ratelimit.Category {
switch e.Type {
case "":
return ratelimit.CategoryError
case transactionType:
return ratelimit.CategoryTransaction
case logEvent.Type:
return ratelimit.CategoryLog
case checkInType:
return ratelimit.CategoryMonitor
default:
return ratelimit.CategoryUnknown
}
}
// NewEvent creates a new Event. // NewEvent creates a new Event.
func NewEvent() *Event { func NewEvent() *Event {
return &Event{ return &Event{
@ -644,7 +659,17 @@ type Log struct {
Attributes map[string]Attribute `json:"attributes,omitempty"` Attributes map[string]Attribute `json:"attributes,omitempty"`
} }
type AttrType string
const (
AttributeInvalid AttrType = ""
AttributeBool AttrType = "boolean"
AttributeInt AttrType = "integer"
AttributeFloat AttrType = "double"
AttributeString AttrType = "string"
)
type Attribute struct { type Attribute struct {
Value any `json:"value"` Value any `json:"value"`
Type string `json:"type"` Type AttrType `json:"type"`
} }

View file

@ -14,12 +14,14 @@ import (
// and, therefore, rate limited. // and, therefore, rate limited.
type Category string type Category string
// Known rate limit categories. As a special case, the CategoryAll applies to // Known rate limit categories that are specified in rate limit headers.
// all known payload types.
const ( const (
CategoryAll Category = "" CategoryUnknown Category = "unknown" // Unknown category should not get rate limited
CategoryAll Category = "" // Special category for empty categories (applies to all)
CategoryError Category = "error" CategoryError Category = "error"
CategoryTransaction Category = "transaction" CategoryTransaction Category = "transaction"
CategoryLog Category = "log_item"
CategoryMonitor Category = "monitor"
) )
// knownCategories is the set of currently known categories. Other categories // knownCategories is the set of currently known categories. Other categories
@ -28,18 +30,30 @@ var knownCategories = map[Category]struct{}{
CategoryAll: {}, CategoryAll: {},
CategoryError: {}, CategoryError: {},
CategoryTransaction: {}, CategoryTransaction: {},
CategoryLog: {},
CategoryMonitor: {},
} }
// String returns the category formatted for debugging. // String returns the category formatted for debugging.
func (c Category) String() string { func (c Category) String() string {
if c == "" { switch c {
case CategoryAll:
return "CategoryAll" return "CategoryAll"
case CategoryError:
return "CategoryError"
case CategoryTransaction:
return "CategoryTransaction"
case CategoryLog:
return "CategoryLog"
case CategoryMonitor:
return "CategoryMonitor"
default:
// For unknown categories, use the original formatting logic
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
} }
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
} }

View file

@ -3,8 +3,10 @@ package sentry
import ( import (
"context" "context"
"fmt" "fmt"
"maps"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/attribute"
@ -30,17 +32,28 @@ const (
LogSeverityFatal int = 21 LogSeverityFatal int = 21
) )
var mapTypesToStr = map[attribute.Type]string{ var mapTypesToStr = map[attribute.Type]AttrType{
attribute.INVALID: "", attribute.INVALID: AttributeInvalid,
attribute.BOOL: "boolean", attribute.BOOL: AttributeBool,
attribute.INT64: "integer", attribute.INT64: AttributeInt,
attribute.FLOAT64: "double", attribute.FLOAT64: AttributeFloat,
attribute.STRING: "string", attribute.STRING: AttributeString,
} }
type sentryLogger struct { type sentryLogger struct {
ctx context.Context
client *Client client *Client
attributes map[string]Attribute attributes map[string]Attribute
mu sync.RWMutex
}
type logEntry struct {
logger *sentryLogger
ctx context.Context
level LogLevel
severity int
attributes map[string]Attribute
shouldPanic bool
} }
// NewLogger returns a Logger that emits logs to Sentry. If logging is turned off, all logs get discarded. // NewLogger returns a Logger that emits logs to Sentry. If logging is turned off, all logs get discarded.
@ -53,7 +66,12 @@ func NewLogger(ctx context.Context) Logger {
client := hub.Client() client := hub.Client()
if client != nil && client.batchLogger != nil { if client != nil && client.batchLogger != nil {
return &sentryLogger{client, make(map[string]Attribute)} return &sentryLogger{
ctx: ctx,
client: client,
attributes: make(map[string]Attribute),
mu: sync.RWMutex{},
}
} }
DebugLogger.Println("fallback to noopLogger: enableLogs disabled") DebugLogger.Println("fallback to noopLogger: enableLogs disabled")
@ -63,11 +81,11 @@ func NewLogger(ctx context.Context) Logger {
func (l *sentryLogger) Write(p []byte) (int, error) { func (l *sentryLogger) Write(p []byte) (int, error) {
// Avoid sending double newlines to Sentry // Avoid sending double newlines to Sentry
msg := strings.TrimRight(string(p), "\n") msg := strings.TrimRight(string(p), "\n")
l.log(context.Background(), LogLevelInfo, LogSeverityInfo, msg) l.Info().Emit(msg)
return len(p), nil return len(p), nil
} }
func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, message string, args ...interface{}) { func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, message string, entryAttrs map[string]Attribute, args ...interface{}) {
if message == "" { if message == "" {
return return
} }
@ -78,71 +96,77 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me
var traceID TraceID var traceID TraceID
var spanID SpanID var spanID SpanID
var span *Span
var user User
span := hub.Scope().span scope := hub.Scope()
if span != nil { if scope != nil {
traceID = span.TraceID scope.mu.Lock()
spanID = span.SpanID span = scope.span
} else { if span != nil {
traceID = hub.Scope().propagationContext.TraceID traceID = span.TraceID
spanID = span.SpanID
} else {
traceID = scope.propagationContext.TraceID
}
user = scope.user
scope.mu.Unlock()
} }
attrs := map[string]Attribute{} attrs := map[string]Attribute{}
if len(args) > 0 { if len(args) > 0 {
attrs["sentry.message.template"] = Attribute{ attrs["sentry.message.template"] = Attribute{
Value: message, Type: "string", Value: message, Type: AttributeString,
} }
for i, p := range args { for i, p := range args {
attrs[fmt.Sprintf("sentry.message.parameters.%d", i)] = Attribute{ attrs[fmt.Sprintf("sentry.message.parameters.%d", i)] = Attribute{
Value: fmt.Sprint(p), Type: "string", Value: fmt.Sprintf("%+v", p), Type: AttributeString,
} }
} }
} }
// If `log` was called with SetAttributes, pass the attributes to attrs l.mu.RLock()
if len(l.attributes) > 0 { for k, v := range l.attributes {
for k, v := range l.attributes { attrs[k] = v
attrs[k] = v }
} l.mu.RUnlock()
// flush attributes from logger after send
clear(l.attributes) for k, v := range entryAttrs {
attrs[k] = v
} }
// Set default attributes // Set default attributes
if release := l.client.options.Release; release != "" { if release := l.client.options.Release; release != "" {
attrs["sentry.release"] = Attribute{Value: release, Type: "string"} attrs["sentry.release"] = Attribute{Value: release, Type: AttributeString}
} }
if environment := l.client.options.Environment; environment != "" { if environment := l.client.options.Environment; environment != "" {
attrs["sentry.environment"] = Attribute{Value: environment, Type: "string"} attrs["sentry.environment"] = Attribute{Value: environment, Type: AttributeString}
} }
if serverName := l.client.options.ServerName; serverName != "" { if serverName := l.client.options.ServerName; serverName != "" {
attrs["sentry.server.address"] = Attribute{Value: serverName, Type: "string"} attrs["sentry.server.address"] = Attribute{Value: serverName, Type: AttributeString}
} else if serverAddr, err := os.Hostname(); err == nil { } else if serverAddr, err := os.Hostname(); err == nil {
attrs["sentry.server.address"] = Attribute{Value: serverAddr, Type: "string"} attrs["sentry.server.address"] = Attribute{Value: serverAddr, Type: AttributeString}
} }
scope := hub.Scope()
if scope != nil { if !user.IsEmpty() {
user := scope.user if user.ID != "" {
if !user.IsEmpty() { attrs["user.id"] = Attribute{Value: user.ID, Type: AttributeString}
if user.ID != "" { }
attrs["user.id"] = Attribute{Value: user.ID, Type: "string"} if user.Name != "" {
} attrs["user.name"] = Attribute{Value: user.Name, Type: AttributeString}
if user.Name != "" { }
attrs["user.name"] = Attribute{Value: user.Name, Type: "string"} if user.Email != "" {
} attrs["user.email"] = Attribute{Value: user.Email, Type: AttributeString}
if user.Email != "" {
attrs["user.email"] = Attribute{Value: user.Email, Type: "string"}
}
} }
} }
if spanID.String() != "0000000000000000" { if span != nil {
attrs["sentry.trace.parent_span_id"] = Attribute{Value: spanID.String(), Type: "string"} attrs["sentry.trace.parent_span_id"] = Attribute{Value: spanID.String(), Type: AttributeString}
} }
if sdkIdentifier := l.client.sdkIdentifier; sdkIdentifier != "" { if sdkIdentifier := l.client.sdkIdentifier; sdkIdentifier != "" {
attrs["sentry.sdk.name"] = Attribute{Value: sdkIdentifier, Type: "string"} attrs["sentry.sdk.name"] = Attribute{Value: sdkIdentifier, Type: AttributeString}
} }
if sdkVersion := l.client.sdkVersion; sdkVersion != "" { if sdkVersion := l.client.sdkVersion; sdkVersion != "" {
attrs["sentry.sdk.version"] = Attribute{Value: sdkVersion, Type: "string"} attrs["sentry.sdk.version"] = Attribute{Value: sdkVersion, Type: AttributeString}
} }
log := &Log{ log := &Log{
@ -168,6 +192,9 @@ func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, me
} }
func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) { func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) {
l.mu.Lock()
defer l.mu.Unlock()
for _, v := range attrs { for _, v := range attrs {
t, ok := mapTypesToStr[v.Value.Type()] t, ok := mapTypesToStr[v.Value.Type()]
if !ok || t == "" { if !ok || t == "" {
@ -182,49 +209,136 @@ func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) {
} }
} }
func (l *sentryLogger) Trace(ctx context.Context, v ...interface{}) { func (l *sentryLogger) Trace() LogEntry {
l.log(ctx, LogLevelTrace, LogSeverityTrace, fmt.Sprint(v...)) return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelTrace,
severity: LogSeverityTrace,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Debug(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelDebug, LogSeverityDebug, fmt.Sprint(v...)) func (l *sentryLogger) Debug() LogEntry {
return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelDebug,
severity: LogSeverityDebug,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Info(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelInfo, LogSeverityInfo, fmt.Sprint(v...)) func (l *sentryLogger) Info() LogEntry {
return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelInfo,
severity: LogSeverityInfo,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Warn(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelWarn, LogSeverityWarning, fmt.Sprint(v...)) func (l *sentryLogger) Warn() LogEntry {
return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelWarn,
severity: LogSeverityWarning,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Error(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelError, LogSeverityError, fmt.Sprint(v...)) func (l *sentryLogger) Error() LogEntry {
return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelError,
severity: LogSeverityError,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Fatal(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...)) func (l *sentryLogger) Fatal() LogEntry {
os.Exit(1) return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelFatal,
severity: LogSeverityFatal,
attributes: make(map[string]Attribute),
}
} }
func (l *sentryLogger) Panic(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...)) func (l *sentryLogger) Panic() LogEntry {
panic(fmt.Sprint(v...)) return &logEntry{
logger: l,
ctx: l.ctx,
level: LogLevelFatal,
severity: LogSeverityFatal,
attributes: make(map[string]Attribute),
shouldPanic: true, // this should panic instead of exit
}
} }
func (l *sentryLogger) Tracef(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelTrace, LogSeverityTrace, format, v...) func (l *sentryLogger) GetCtx() context.Context {
return l.ctx
} }
func (l *sentryLogger) Debugf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelDebug, LogSeverityDebug, format, v...) func (e *logEntry) WithCtx(ctx context.Context) LogEntry {
return &logEntry{
logger: e.logger,
ctx: ctx,
level: e.level,
severity: e.severity,
attributes: maps.Clone(e.attributes),
shouldPanic: e.shouldPanic,
}
} }
func (l *sentryLogger) Infof(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelInfo, LogSeverityInfo, format, v...) func (e *logEntry) String(key, value string) LogEntry {
e.attributes[key] = Attribute{Value: value, Type: AttributeString}
return e
} }
func (l *sentryLogger) Warnf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelWarn, LogSeverityWarning, format, v...) func (e *logEntry) Int(key string, value int) LogEntry {
e.attributes[key] = Attribute{Value: int64(value), Type: AttributeInt}
return e
} }
func (l *sentryLogger) Errorf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelError, LogSeverityError, format, v...) func (e *logEntry) Int64(key string, value int64) LogEntry {
e.attributes[key] = Attribute{Value: value, Type: AttributeInt}
return e
} }
func (l *sentryLogger) Fatalf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...) func (e *logEntry) Float64(key string, value float64) LogEntry {
os.Exit(1) e.attributes[key] = Attribute{Value: value, Type: AttributeFloat}
return e
} }
func (l *sentryLogger) Panicf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...) func (e *logEntry) Bool(key string, value bool) LogEntry {
panic(fmt.Sprint(v...)) e.attributes[key] = Attribute{Value: value, Type: AttributeBool}
return e
}
func (e *logEntry) Emit(args ...interface{}) {
e.logger.log(e.ctx, e.level, e.severity, fmt.Sprint(args...), e.attributes)
if e.level == LogLevelFatal {
if e.shouldPanic {
panic(fmt.Sprint(args...))
}
os.Exit(1)
}
}
func (e *logEntry) Emitf(format string, args ...interface{}) {
e.logger.log(e.ctx, e.level, e.severity, format, e.attributes, args...)
if e.level == LogLevelFatal {
if e.shouldPanic {
formattedMessage := fmt.Sprintf(format, args...)
panic(formattedMessage)
}
os.Exit(1)
}
} }

View file

@ -11,55 +11,94 @@ import (
// Fallback, no-op logger if logging is disabled. // Fallback, no-op logger if logging is disabled.
type noopLogger struct{} type noopLogger struct{}
func (*noopLogger) Trace(_ context.Context, _ ...interface{}) { // noopLogEntry implements LogEntry for the no-op logger.
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace) type noopLogEntry struct {
level LogLevel
shouldPanic bool
} }
func (*noopLogger) Debug(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug) func (n *noopLogEntry) WithCtx(_ context.Context) LogEntry {
return n
} }
func (*noopLogger) Info(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo) func (n *noopLogEntry) String(_, _ string) LogEntry {
return n
} }
func (*noopLogger) Warn(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn) func (n *noopLogEntry) Int(_ string, _ int) LogEntry {
return n
} }
func (*noopLogger) Error(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError) func (n *noopLogEntry) Int64(_ string, _ int64) LogEntry {
return n
} }
func (*noopLogger) Fatal(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal) func (n *noopLogEntry) Float64(_ string, _ float64) LogEntry {
os.Exit(1) return n
} }
func (*noopLogger) Panic(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal) func (n *noopLogEntry) Bool(_ string, _ bool) LogEntry {
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)) return n
} }
func (*noopLogger) Tracef(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace) func (n *noopLogEntry) Attributes(_ ...attribute.Builder) LogEntry {
return n
} }
func (*noopLogger) Debugf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug) func (n *noopLogEntry) Emit(args ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level)
if n.level == LogLevelFatal {
if n.shouldPanic {
panic(args)
}
os.Exit(1)
}
} }
func (*noopLogger) Infof(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo) func (n *noopLogEntry) Emitf(message string, args ...interface{}) {
DebugLogger.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...))
}
os.Exit(1)
}
} }
func (*noopLogger) Warnf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn) func (n *noopLogger) GetCtx() context.Context { return context.Background() }
func (*noopLogger) Trace() LogEntry {
return &noopLogEntry{level: LogLevelTrace}
} }
func (*noopLogger) Errorf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError) func (*noopLogger) Debug() LogEntry {
return &noopLogEntry{level: LogLevelDebug}
} }
func (*noopLogger) Fatalf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal) func (*noopLogger) Info() LogEntry {
os.Exit(1) return &noopLogEntry{level: LogLevelInfo}
} }
func (*noopLogger) Panicf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal) func (*noopLogger) Warn() LogEntry {
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)) return &noopLogEntry{level: LogLevelWarn}
} }
func (*noopLogger) Error() LogEntry {
return &noopLogEntry{level: LogLevelError}
}
func (*noopLogger) Fatal() LogEntry {
return &noopLogEntry{level: LogLevelFatal}
}
func (*noopLogger) Panic() LogEntry {
return &noopLogEntry{level: LogLevelFatal, shouldPanic: true}
}
func (*noopLogger) SetAttributes(...attribute.Builder) { func (*noopLogger) SetAttributes(...attribute.Builder) {
DebugLogger.Printf("No attributes attached. Turn on logging via EnableLogs") DebugLogger.Printf("No attributes attached. Turn on logging via EnableLogs")
} }
func (*noopLogger) Write(_ []byte) (n int, err error) { func (*noopLogger) Write(_ []byte) (n int, err error) {
return 0, fmt.Errorf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo) return 0, fmt.Errorf("log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
} }

View file

@ -304,6 +304,9 @@ func (scope *Scope) SetPropagationContext(propagationContext PropagationContext)
// GetSpan returns the span from the current scope. // GetSpan returns the span from the current scope.
func (scope *Scope) GetSpan() *Span { func (scope *Scope) GetSpan() *Span {
scope.mu.RLock()
defer scope.mu.RUnlock()
return scope.span return scope.span
} }

View file

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

View file

@ -262,17 +262,6 @@ func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.R
) )
} }
func categoryFor(eventType string) ratelimit.Category {
switch eventType {
case "":
return ratelimit.CategoryError
case transactionType:
return ratelimit.CategoryTransaction
default:
return ratelimit.Category(eventType)
}
}
// ================================ // ================================
// HTTPTransport // HTTPTransport
// ================================ // ================================
@ -303,7 +292,8 @@ type HTTPTransport struct {
// current in-flight items and starts a new batch for subsequent events. // current in-flight items and starts a new batch for subsequent events.
buffer chan batch buffer chan batch
start sync.Once startOnce sync.Once
closeOnce sync.Once
// Size of the transport buffer. Defaults to 30. // Size of the transport buffer. Defaults to 30.
BufferSize int BufferSize int
@ -364,7 +354,7 @@ func (t *HTTPTransport) Configure(options ClientOptions) {
} }
} }
t.start.Do(func() { t.startOnce.Do(func() {
go t.worker() go t.worker()
}) })
} }
@ -380,7 +370,7 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
return return
} }
category := categoryFor(event.Type) category := event.toCategory()
if t.disabled(category) { if t.disabled(category) {
return return
@ -440,11 +430,9 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
// have the SDK send events over the network synchronously, configure it to use // have the SDK send events over the network synchronously, configure it to use
// the HTTPSyncTransport in the call to Init. // the HTTPSyncTransport in the call to Init.
func (t *HTTPTransport) Flush(timeout time.Duration) bool { func (t *HTTPTransport) Flush(timeout time.Duration) bool {
timeoutCh := make(chan struct{}) ctx, cancel := context.WithTimeout(context.Background(), timeout)
time.AfterFunc(timeout, func() { defer cancel()
close(timeoutCh) return t.FlushWithContext(ctx)
})
return t.flushInternal(timeoutCh)
} }
// FlushWithContext works like Flush, but it accepts a context.Context instead of a timeout. // FlushWithContext works like Flush, but it accepts a context.Context instead of a timeout.
@ -506,7 +494,9 @@ fail:
// Close should be called after Flush and before terminating the program // Close should be called after Flush and before terminating the program
// otherwise some events may be lost. // otherwise some events may be lost.
func (t *HTTPTransport) Close() { func (t *HTTPTransport) Close() {
close(t.done) t.closeOnce.Do(func() {
close(t.done)
})
} }
func (t *HTTPTransport) worker() { func (t *HTTPTransport) worker() {
@ -652,7 +642,7 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
return return
} }
if t.disabled(categoryFor(event.Type)) { if t.disabled(event.toCategory()) {
return return
} }

43
vendor/github.com/mattn/go-runewidth/benchstat.txt generated vendored Normal file
View file

@ -0,0 +1,43 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
String1WidthAll/regular-8 108.92m ± 0% 35.09m ± 3% -67.78% (p=0.002 n=6)
String1WidthAll/lut-8 93.97m ± 0% 18.70m ± 0% -80.10% (p=0.002 n=6)
String1Width768/regular-8 60.62µ ± 1% 11.54µ ± 0% -80.97% (p=0.002 n=6)
String1Width768/lut-8 60.66µ ± 1% 11.43µ ± 0% -81.16% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 115.13m ± 1% 40.79m ± 8% -64.57% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 93.65m ± 0% 18.70m ± 2% -80.03% (p=0.002 n=6)
String1Width768EastAsian/regular-8 75.32µ ± 0% 23.49µ ± 0% -68.82% (p=0.002 n=6)
String1Width768EastAsian/lut-8 60.76µ ± 0% 11.50µ ± 0% -81.07% (p=0.002 n=6)
geomean 2.562m 604.5µ -76.41%
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
String1WidthAll/regular-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1WidthAll/lut-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1Width768/regular-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1Width768/lut-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 106.3Mi ± 0% 0.0Mi ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/regular-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/lut-8 75.00Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.002 n=6)
geomean 2.790Mi ? ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
String1WidthAll/regular-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1WidthAll/lut-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1Width768/regular-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1Width768/lut-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/regular-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1WidthAllEastAsian/lut-8 3.342M ± 0% 0.000M ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/regular-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
String1Width768EastAsian/lut-8 2.304k ± 0% 0.000k ± 0% -100.00% (p=0.002 n=6)
geomean 87.75k ? ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

54
vendor/github.com/mattn/go-runewidth/new.txt generated vendored Normal file
View file

@ -0,0 +1,54 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
BenchmarkString1WidthAll/regular-8 33 35033923 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 34965112 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 36307234 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 35007705 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 33 35154182 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/regular-8 34 35155400 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18688500 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18712474 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18700211 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 62 18694179 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 62 18708392 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAll/lut-8 63 18770608 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104137 11526 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103986 11540 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104079 11552 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103963 11530 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 103714 11538 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/regular-8 104181 11537 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105150 11420 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 104778 11423 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105069 11422 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105127 11475 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 104742 11433 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768/lut-8 105163 11432 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40723347 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40790299 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40801338 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40798216 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 44135253 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 28 40779546 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18694165 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18685047 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18689273 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 19150346 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 63 19126154 ns/op 0 B/op 0 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 62 18712619 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 50775 23595 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51061 23563 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51057 23492 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51138 23445 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51195 23469 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/regular-8 51087 23482 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104559 11549 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104508 11483 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104296 11503 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104606 11485 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104588 11495 ns/op 0 B/op 0 allocs/op
BenchmarkString1Width768EastAsian/lut-8 104602 11518 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/mattn/go-runewidth 64.455s

54
vendor/github.com/mattn/go-runewidth/old.txt generated vendored Normal file
View file

@ -0,0 +1,54 @@
goos: darwin
goarch: arm64
pkg: github.com/mattn/go-runewidth
cpu: Apple M2
BenchmarkString1WidthAll/regular-8 10 108559258 ns/op 111412145 B/op 3342342 allocs/op
BenchmarkString1WidthAll/regular-8 10 108968079 ns/op 111412364 B/op 3342343 allocs/op
BenchmarkString1WidthAll/regular-8 10 108890338 ns/op 111412388 B/op 3342344 allocs/op
BenchmarkString1WidthAll/regular-8 10 108940704 ns/op 111412584 B/op 3342346 allocs/op
BenchmarkString1WidthAll/regular-8 10 108632796 ns/op 111412348 B/op 3342343 allocs/op
BenchmarkString1WidthAll/regular-8 10 109354546 ns/op 111412777 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93844406 ns/op 111412569 B/op 3342345 allocs/op
BenchmarkString1WidthAll/lut-8 12 93991080 ns/op 111412512 B/op 3342344 allocs/op
BenchmarkString1WidthAll/lut-8 12 93980632 ns/op 111412413 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 94004083 ns/op 111412396 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93959795 ns/op 111412445 B/op 3342343 allocs/op
BenchmarkString1WidthAll/lut-8 12 93846198 ns/op 111412556 B/op 3342345 allocs/op
BenchmarkString1Width768/regular-8 19785 60696 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19824 60520 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19832 60547 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19778 60543 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19842 61142 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/regular-8 19780 60696 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19598 61161 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19731 60707 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19738 60626 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19764 60670 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19797 60642 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768/lut-8 19738 60608 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115080431 ns/op 111412458 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 114908880 ns/op 111412476 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115077134 ns/op 111412540 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115175292 ns/op 111412467 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115792653 ns/op 111412362 B/op 3342344 allocs/op
BenchmarkString1WidthAllEastAsian/regular-8 9 115255417 ns/op 111412572 B/op 3342346 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93761542 ns/op 111412538 B/op 3342345 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 94089990 ns/op 111412440 B/op 3342343 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93721410 ns/op 111412514 B/op 3342344 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93572951 ns/op 111412329 B/op 3342342 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93536052 ns/op 111412206 B/op 3342341 allocs/op
BenchmarkString1WidthAllEastAsian/lut-8 12 93532365 ns/op 111412412 B/op 3342343 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15904 75401 ns/op 76800 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15932 75449 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15944 75181 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15963 75311 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15879 75292 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/regular-8 15955 75334 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19692 60692 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19712 60699 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19741 60819 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19771 60653 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19737 61027 ns/op 76801 B/op 2304 allocs/op
BenchmarkString1Width768EastAsian/lut-8 19657 60820 ns/op 76801 B/op 2304 allocs/op
PASS
ok github.com/mattn/go-runewidth 76.165s

View file

@ -4,7 +4,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/rivo/uniseg" "github.com/clipperhouse/uax29/v2/graphemes"
) )
//go:generate go run script/generate.go //go:generate go run script/generate.go
@ -64,6 +64,9 @@ func inTable(r rune, t table) bool {
if r < t[0].first { if r < t[0].first {
return false return false
} }
if r > t[len(t)-1].last {
return false
}
bot := 0 bot := 0
top := len(t) - 1 top := len(t) - 1
@ -175,10 +178,10 @@ func (c *Condition) CreateLUT() {
// StringWidth return width as you can see // StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) { func (c *Condition) StringWidth(s string) (width int) {
g := uniseg.NewGraphemes(s) g := graphemes.FromString(s)
for g.Next() { for g.Next() {
var chWidth int var chWidth int
for _, r := range g.Runes() { for _, r := range g.Value() {
chWidth = c.RuneWidth(r) chWidth = c.RuneWidth(r)
if chWidth > 0 { if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune. break // Our best guess at this point is to use the width of the first non-zero-width rune.
@ -197,17 +200,17 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
w -= c.StringWidth(tail) w -= c.StringWidth(tail)
var width int var width int
pos := len(s) pos := len(s)
g := uniseg.NewGraphemes(s) g := graphemes.FromString(s)
for g.Next() { for g.Next() {
var chWidth int var chWidth int
for _, r := range g.Runes() { for _, r := range g.Value() {
chWidth = c.RuneWidth(r) chWidth = c.RuneWidth(r)
if chWidth > 0 { if chWidth > 0 {
break // See StringWidth() for details. break // See StringWidth() for details.
} }
} }
if width+chWidth > w { if width+chWidth > w {
pos, _ = g.Positions() pos = g.Start()
break break
} }
width += chWidth width += chWidth
@ -224,10 +227,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
var width int var width int
pos := len(s) pos := len(s)
g := uniseg.NewGraphemes(s) g := graphemes.FromString(s)
for g.Next() { for g.Next() {
var chWidth int var chWidth int
for _, r := range g.Runes() { for _, r := range g.Value() {
chWidth = c.RuneWidth(r) chWidth = c.RuneWidth(r)
if chWidth > 0 { if chWidth > 0 {
break // See StringWidth() for details. break // See StringWidth() for details.
@ -236,10 +239,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
if width+chWidth > w { if width+chWidth > w {
if width < w { if width < w {
_, pos = g.Positions() pos = g.End()
prefix += strings.Repeat(" ", width+chWidth-w) prefix += strings.Repeat(" ", width+chWidth-w)
} else { } else {
pos, _ = g.Positions() pos = g.Start()
} }
break break

View file

@ -4,6 +4,7 @@
package runewidth package runewidth
import ( import (
"os"
"syscall" "syscall"
) )
@ -14,6 +15,11 @@ var (
// IsEastAsian return true if the current locale is CJK // IsEastAsian return true if the current locale is CJK
func IsEastAsian() bool { func IsEastAsian() bool {
if os.Getenv("WT_SESSION") != "" {
// Windows Terminal always not use East Asian Ambiguous Width(s).
return false
}
r1, _, _ := procGetConsoleOutputCP.Call() r1, _, _ := procGetConsoleOutputCP.Call()
if r1 == 0 { if r1 == 0 {
return false return false

View file

@ -351,6 +351,8 @@ For example the TDM-GCC Toolchain can be found [here](https://jmeubank.github.io
# User Authentication # User Authentication
***This is deprecated***
This package supports the SQLite User Authentication module. This package supports the SQLite User Authentication module.
## Compile ## Compile

File diff suppressed because it is too large Load diff

View file

@ -134,7 +134,7 @@ extern "C" {
** **
** Since [version 3.6.18] ([dateof:3.6.18]), ** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the ** SQLite source code has been stored in the
** <a href="http://www.fossil-scm.org/">Fossil configuration management ** <a href="http://fossil-scm.org/">Fossil configuration management
** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite ** a string which identifies a particular check-in of SQLite
** within its configuration management system. ^The SQLITE_SOURCE_ID ** within its configuration management system. ^The SQLITE_SOURCE_ID
@ -147,9 +147,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.49.1" #define SQLITE_VERSION "3.50.4"
#define SQLITE_VERSION_NUMBER 3049001 #define SQLITE_VERSION_NUMBER 3050004
#define SQLITE_SOURCE_ID "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70" #define SQLITE_SOURCE_ID "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -1164,6 +1164,12 @@ struct sqlite3_io_methods {
** the value that M is to be set to. Before returning, the 32-bit signed ** the value that M is to be set to. Before returning, the 32-bit signed
** integer is overwritten with the previous value of M. ** integer is overwritten with the previous value of M.
** **
** <li>[[SQLITE_FCNTL_BLOCK_ON_CONNECT]]
** The [SQLITE_FCNTL_BLOCK_ON_CONNECT] opcode is used to configure the
** VFS to block when taking a SHARED lock to connect to a wal mode database.
** This is used to implement the functionality associated with
** SQLITE_SETLK_BLOCK_ON_CONNECT.
**
** <li>[[SQLITE_FCNTL_DATA_VERSION]] ** <li>[[SQLITE_FCNTL_DATA_VERSION]]
** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to ** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to
** a database file. The argument is a pointer to a 32-bit unsigned integer. ** a database file. The argument is a pointer to a 32-bit unsigned integer.
@ -1260,6 +1266,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_CKSM_FILE 41 #define SQLITE_FCNTL_CKSM_FILE 41
#define SQLITE_FCNTL_RESET_CACHE 42 #define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43 #define SQLITE_FCNTL_NULL_IO 43
#define SQLITE_FCNTL_BLOCK_ON_CONNECT 44
/* deprecated names */ /* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@ -1990,13 +1997,16 @@ struct sqlite3_mem_methods {
** **
** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt> ** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt>
** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine ** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine
** the default size of lookaside memory on each [database connection]. ** the default size of [lookaside memory] on each [database connection].
** The first argument is the ** The first argument is the
** size of each lookaside buffer slot and the second is the number of ** size of each lookaside buffer slot ("sz") and the second is the number of
** slots allocated to each database connection.)^ ^(SQLITE_CONFIG_LOOKASIDE ** slots allocated to each database connection ("cnt").)^
** sets the <i>default</i> lookaside size. The [SQLITE_DBCONFIG_LOOKASIDE] ** ^(SQLITE_CONFIG_LOOKASIDE sets the <i>default</i> lookaside size.
** option to [sqlite3_db_config()] can be used to change the lookaside ** The [SQLITE_DBCONFIG_LOOKASIDE] option to [sqlite3_db_config()] can
** configuration on individual connections.)^ </dd> ** be used to change the lookaside configuration on individual connections.)^
** The [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to change the
** default lookaside configuration at compile-time.
** </dd>
** **
** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt> ** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt>
** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is ** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is
@ -2233,31 +2243,50 @@ struct sqlite3_mem_methods {
** [[SQLITE_DBCONFIG_LOOKASIDE]] ** [[SQLITE_DBCONFIG_LOOKASIDE]]
** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt> ** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt>
** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the ** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the
** configuration of the lookaside memory allocator within a database ** configuration of the [lookaside memory allocator] within a database
** connection. ** connection.
** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i> ** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i>
** in the [DBCONFIG arguments|usual format]. ** in the [DBCONFIG arguments|usual format].
** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two, ** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two,
** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE ** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE
** should have a total of five parameters. ** should have a total of five parameters.
** ^The first argument (the third parameter to [sqlite3_db_config()] is a ** <ol>
** <li><p>The first argument ("buf") is a
** pointer to a memory buffer to use for lookaside memory. ** pointer to a memory buffer to use for lookaside memory.
** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb ** The first argument may be NULL in which case SQLite will allocate the
** may be NULL in which case SQLite will allocate the ** lookaside buffer itself using [sqlite3_malloc()].
** lookaside buffer itself using [sqlite3_malloc()]. ^The second argument is the ** <li><P>The second argument ("sz") is the
** size of each lookaside buffer slot. ^The third argument is the number of ** size of each lookaside buffer slot. Lookaside is disabled if "sz"
** slots. The size of the buffer in the first argument must be greater than ** is less than 8. The "sz" argument should be a multiple of 8 less than
** or equal to the product of the second and third arguments. The buffer ** 65536. If "sz" does not meet this constraint, it is reduced in size until
** must be aligned to an 8-byte boundary. ^If the second argument to ** it does.
** SQLITE_DBCONFIG_LOOKASIDE is not a multiple of 8, it is internally ** <li><p>The third argument ("cnt") is the number of slots. Lookaside is disabled
** rounded down to the next smaller multiple of 8. ^(The lookaside memory ** if "cnt"is less than 1. The "cnt" value will be reduced, if necessary, so
** that the product of "sz" and "cnt" does not exceed 2,147,418,112. The "cnt"
** parameter is usually chosen so that the product of "sz" and "cnt" is less
** than 1,000,000.
** </ol>
** <p>If the "buf" argument is not NULL, then it must
** point to a memory buffer with a size that is greater than
** or equal to the product of "sz" and "cnt".
** The buffer must be aligned to an 8-byte boundary.
** The lookaside memory
** configuration for a database connection can only be changed when that ** configuration for a database connection can only be changed when that
** connection is not currently using lookaside memory, or in other words ** connection is not currently using lookaside memory, or in other words
** when the "current value" returned by ** when the value returned by [SQLITE_DBSTATUS_LOOKASIDE_USED] is zero.
** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero.
** Any attempt to change the lookaside memory configuration when lookaside ** Any attempt to change the lookaside memory configuration when lookaside
** memory is in use leaves the configuration unchanged and returns ** memory is in use leaves the configuration unchanged and returns
** [SQLITE_BUSY].)^</dd> ** [SQLITE_BUSY].
** If the "buf" argument is NULL and an attempt
** to allocate memory based on "sz" and "cnt" fails, then
** lookaside is silently disabled.
** <p>
** The [SQLITE_CONFIG_LOOKASIDE] configuration option can be used to set the
** default lookaside configuration at initialization. The
** [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to set the default lookaside
** configuration at compile-time. Typical values for lookaside are 1200 for
** "sz" and 40 to 100 for "cnt".
** </dd>
** **
** [[SQLITE_DBCONFIG_ENABLE_FKEY]] ** [[SQLITE_DBCONFIG_ENABLE_FKEY]]
** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt> ** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt>
@ -2994,6 +3023,44 @@ SQLITE_API int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*);
*/ */
SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms); SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms);
/*
** CAPI3REF: Set the Setlk Timeout
** METHOD: sqlite3
**
** This routine is only useful in SQLITE_ENABLE_SETLK_TIMEOUT builds. If
** the VFS supports blocking locks, it sets the timeout in ms used by
** eligible locks taken on wal mode databases by the specified database
** handle. In non-SQLITE_ENABLE_SETLK_TIMEOUT builds, or if the VFS does
** not support blocking locks, this function is a no-op.
**
** Passing 0 to this function disables blocking locks altogether. Passing
** -1 to this function requests that the VFS blocks for a long time -
** indefinitely if possible. The results of passing any other negative value
** are undefined.
**
** Internally, each SQLite database handle store two timeout values - the
** busy-timeout (used for rollback mode databases, or if the VFS does not
** support blocking locks) and the setlk-timeout (used for blocking locks
** on wal-mode databases). The sqlite3_busy_timeout() method sets both
** values, this function sets only the setlk-timeout value. Therefore,
** to configure separate busy-timeout and setlk-timeout values for a single
** database handle, call sqlite3_busy_timeout() followed by this function.
**
** Whenever the number of connections to a wal mode database falls from
** 1 to 0, the last connection takes an exclusive lock on the database,
** then checkpoints and deletes the wal file. While it is doing this, any
** new connection that tries to read from the database fails with an
** SQLITE_BUSY error. Or, if the SQLITE_SETLK_BLOCK_ON_CONNECT flag is
** passed to this API, the new connection blocks until the exclusive lock
** has been released.
*/
SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags);
/*
** CAPI3REF: Flags for sqlite3_setlk_timeout()
*/
#define SQLITE_SETLK_BLOCK_ON_CONNECT 0x01
/* /*
** CAPI3REF: Convenience Routines For Running Queries ** CAPI3REF: Convenience Routines For Running Queries
** METHOD: sqlite3 ** METHOD: sqlite3
@ -4013,7 +4080,7 @@ SQLITE_API sqlite3_file *sqlite3_database_file_object(const char*);
** **
** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of ** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of
** database filename D with corresponding journal file J and WAL file W and ** database filename D with corresponding journal file J and WAL file W and
** with N URI parameters key/values pairs in the array P. The result from ** an array P of N URI Key/Value pairs. The result from
** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that ** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that
** is safe to pass to routines like: ** is safe to pass to routines like:
** <ul> ** <ul>
@ -4694,7 +4761,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants, ** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants,
** literals may be replaced by a [parameter] that matches one of following ** literals may be replaced by a [parameter] that matches one of the following
** templates: ** templates:
** **
** <ul> ** <ul>
@ -4739,7 +4806,7 @@ typedef struct sqlite3_context sqlite3_context;
** **
** [[byte-order determination rules]] ^The byte-order of ** [[byte-order determination rules]] ^The byte-order of
** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF) ** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF)
** found in first character, which is removed, or in the absence of a BOM ** found in the first character, which is removed, or in the absence of a BOM
** the byte order is the native byte order of the host ** the byte order is the native byte order of the host
** machine for sqlite3_bind_text16() or the byte order specified in ** machine for sqlite3_bind_text16() or the byte order specified in
** the 6th parameter for sqlite3_bind_text64().)^ ** the 6th parameter for sqlite3_bind_text64().)^
@ -4759,7 +4826,7 @@ typedef struct sqlite3_context sqlite3_context;
** or sqlite3_bind_text16() or sqlite3_bind_text64() then ** or sqlite3_bind_text16() or sqlite3_bind_text64() then
** that parameter must be the byte offset ** that parameter must be the byte offset
** where the NUL terminator would occur assuming the string were NUL ** where the NUL terminator would occur assuming the string were NUL
** terminated. If any NUL characters occurs at byte offsets less than ** terminated. If any NUL characters occur at byte offsets less than
** the value of the fourth parameter then the resulting string value will ** the value of the fourth parameter then the resulting string value will
** contain embedded NULs. The result of expressions involving strings ** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined. ** with embedded NULs is undefined.
@ -4971,7 +5038,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt ** METHOD: sqlite3_stmt
** **
** ^These routines provide a means to determine the database, table, and ** ^These routines provide a means to determine the database, table, and
** table column that is the origin of a particular result column in ** table column that is the origin of a particular result column in a
** [SELECT] statement. ** [SELECT] statement.
** ^The name of the database or table or column can be returned as ** ^The name of the database or table or column can be returned as
** either a UTF-8 or UTF-16 string. ^The _database_ routines return ** either a UTF-8 or UTF-16 string. ^The _database_ routines return
@ -5109,7 +5176,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int);
** other than [SQLITE_ROW] before any subsequent invocation of ** other than [SQLITE_ROW] before any subsequent invocation of
** sqlite3_step(). Failure to reset the prepared statement using ** sqlite3_step(). Failure to reset the prepared statement using
** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from ** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from
** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1], ** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1]),
** sqlite3_step() began ** sqlite3_step() began
** calling [sqlite3_reset()] automatically in this circumstance rather ** calling [sqlite3_reset()] automatically in this circumstance rather
** than returning [SQLITE_MISUSE]. This is not considered a compatibility ** than returning [SQLITE_MISUSE]. This is not considered a compatibility
@ -5540,8 +5607,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** **
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for ** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be ** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of ** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL ** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state. ** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of ** Without this flag, an attacker might be able to modify the schema of
** a database file to include invocations of the function with parameters ** a database file to include invocations of the function with parameters
@ -5572,7 +5639,7 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** [user-defined window functions|available here]. ** [user-defined window functions|available here].
** **
** ^(If the final parameter to sqlite3_create_function_v2() or ** ^(If the final parameter to sqlite3_create_function_v2() or
** sqlite3_create_window_function() is not NULL, then it is destructor for ** sqlite3_create_window_function() is not NULL, then it is the destructor for
** the application data pointer. The destructor is invoked when the function ** the application data pointer. The destructor is invoked when the function
** is deleted, either by being overloaded or when the database connection ** is deleted, either by being overloaded or when the database connection
** closes.)^ ^The destructor is also invoked if the call to ** closes.)^ ^The destructor is also invoked if the call to
@ -5972,7 +6039,7 @@ SQLITE_API unsigned int sqlite3_value_subtype(sqlite3_value*);
** METHOD: sqlite3_value ** METHOD: sqlite3_value
** **
** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value] ** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value]
** object D and returns a pointer to that copy. ^The [sqlite3_value] returned ** object V and returns a pointer to that copy. ^The [sqlite3_value] returned
** is a [protected sqlite3_value] object even if the input is not. ** is a [protected sqlite3_value] object even if the input is not.
** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a ** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
** memory allocation fails. ^If V is a [pointer value], then the result ** memory allocation fails. ^If V is a [pointer value], then the result
@ -6010,7 +6077,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs. ** allocation error occurs.
** **
** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is ** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
** determined by the N parameter on first successful call. Changing the ** determined by the N parameter on the first successful call. Changing the
** value of N in any subsequent call to sqlite3_aggregate_context() within ** value of N in any subsequent call to sqlite3_aggregate_context() within
** the same aggregate function instance will not resize the memory ** the same aggregate function instance will not resize the memory
** allocation.)^ Within the xFinal callback, it is customary to set ** allocation.)^ Within the xFinal callback, it is customary to set
@ -6172,7 +6239,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
** **
** Security Warning: These interfaces should not be exposed in scripting ** Security Warning: These interfaces should not be exposed in scripting
** languages or in other circumstances where it might be possible for an ** languages or in other circumstances where it might be possible for an
** an attacker to invoke them. Any agent that can invoke these interfaces ** attacker to invoke them. Any agent that can invoke these interfaces
** can probably also take control of the process. ** can probably also take control of the process.
** **
** Database connection client data is only available for SQLite ** Database connection client data is only available for SQLite
@ -6286,7 +6353,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** pointed to by the 2nd parameter are taken as the application-defined ** pointed to by the 2nd parameter are taken as the application-defined
** function result. If the 3rd parameter is non-negative, then it ** function result. If the 3rd parameter is non-negative, then it
** must be the byte offset into the string where the NUL terminator would ** must be the byte offset into the string where the NUL terminator would
** appear if the string where NUL terminated. If any NUL characters occur ** appear if the string were NUL terminated. If any NUL characters occur
** in the string at a byte offset that is less than the value of the 3rd ** in the string at a byte offset that is less than the value of the 3rd
** parameter, then the resulting string will contain embedded NULs and the ** parameter, then the resulting string will contain embedded NULs and the
** result of expressions operating on strings with embedded NULs is undefined. ** result of expressions operating on strings with embedded NULs is undefined.
@ -6344,7 +6411,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** string and preferably a string literal. The sqlite3_result_pointer() ** string and preferably a string literal. The sqlite3_result_pointer()
** routine is part of the [pointer passing interface] added for SQLite 3.20.0. ** routine is part of the [pointer passing interface] added for SQLite 3.20.0.
** **
** If these routines are called from within the different thread ** If these routines are called from within a different thread
** than the one containing the application-defined function that received ** than the one containing the application-defined function that received
** the [sqlite3_context] pointer, the results are undefined. ** the [sqlite3_context] pointer, the results are undefined.
*/ */
@ -6750,7 +6817,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name ** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
** for the N-th database on database connection D, or a NULL pointer of N is ** for the N-th database on database connection D, or a NULL pointer if N is
** out of range. An N value of 0 means the main database file. An N of 1 is ** out of range. An N value of 0 means the main database file. An N of 1 is
** the "temp" schema. Larger values of N correspond to various ATTACH-ed ** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** databases. ** databases.
@ -6845,7 +6912,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_READ state means that the database is currently ** <dd>The SQLITE_TXN_READ state means that the database is currently
** in a read transaction. Content has been read from the database file ** in a read transaction. Content has been read from the database file
** but nothing in the database file has changed. The transaction state ** but nothing in the database file has changed. The transaction state
** will advanced to SQLITE_TXN_WRITE if any changes occur and there are ** will be advanced to SQLITE_TXN_WRITE if any changes occur and there are
** no other conflicting concurrent write transactions. The transaction ** no other conflicting concurrent write transactions. The transaction
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or ** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [COMMIT].</dd> ** [COMMIT].</dd>
@ -6854,7 +6921,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_WRITE state means that the database is currently ** <dd>The SQLITE_TXN_WRITE state means that the database is currently
** in a write transaction. Content has been written to the database file ** in a write transaction. Content has been written to the database file
** but has not yet committed. The transaction state will change to ** but has not yet committed. The transaction state will change to
** to SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd> ** SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
*/ */
#define SQLITE_TXN_NONE 0 #define SQLITE_TXN_NONE 0
#define SQLITE_TXN_READ 1 #define SQLITE_TXN_READ 1
@ -7005,6 +7072,8 @@ SQLITE_API int sqlite3_autovacuum_pages(
** **
** ^The second argument is a pointer to the function to invoke when a ** ^The second argument is a pointer to the function to invoke when a
** row is updated, inserted or deleted in a rowid table. ** row is updated, inserted or deleted in a rowid table.
** ^The update hook is disabled by invoking sqlite3_update_hook()
** with a NULL pointer as the second parameter.
** ^The first argument to the callback is a copy of the third argument ** ^The first argument to the callback is a copy of the third argument
** to sqlite3_update_hook(). ** to sqlite3_update_hook().
** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE], ** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE],
@ -7133,7 +7202,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size ** CAPI3REF: Impose A Limit On Heap Size
** **
** These interfaces impose limits on the amount of heap memory that will be ** These interfaces impose limits on the amount of heap memory that will be
** by all database connections within a single process. ** used by all database connections within a single process.
** **
** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the ** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the
** soft limit on the amount of heap memory that may be allocated by SQLite. ** soft limit on the amount of heap memory that may be allocated by SQLite.
@ -7191,7 +7260,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** </ul>)^ ** </ul>)^
** **
** The circumstances under which SQLite will enforce the heap limits may ** The circumstances under which SQLite will enforce the heap limits may
** changes in future releases of SQLite. ** change in future releases of SQLite.
*/ */
SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N);
SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N); SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N);
@ -7306,8 +7375,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc. ** ^The entry point is zProc.
** ^(zProc may be 0, in which case SQLite will try to come up with an ** ^(zProc may be 0, in which case SQLite will try to come up with an
** entry point name on its own. It first tries "sqlite3_extension_init". ** entry point name on its own. It first tries "sqlite3_extension_init".
** If that does not work, it constructs a name "sqlite3_X_init" where the ** If that does not work, it constructs a name "sqlite3_X_init" where
** X is consists of the lower-case equivalent of all ASCII alphabetic ** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following ** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^ ** "." and omitting any initial "lib".)^
** ^The sqlite3_load_extension() interface returns ** ^The sqlite3_load_extension() interface returns
@ -7378,7 +7447,7 @@ SQLITE_API int sqlite3_enable_load_extension(sqlite3 *db, int onoff);
** ^(Even though the function prototype shows that xEntryPoint() takes ** ^(Even though the function prototype shows that xEntryPoint() takes
** no arguments and returns void, SQLite invokes xEntryPoint() with three ** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the ** arguments and expects an integer result as if the signature of the
** entry point where as follows: ** entry point were as follows:
** **
** <blockquote><pre> ** <blockquote><pre>
** &nbsp; int xEntryPoint( ** &nbsp; int xEntryPoint(
@ -7542,7 +7611,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The ** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag ** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** is left in its default setting of false, the constraint will always be ** is left in its default setting of false, the constraint will always be
** checked separately in byte code. If the omit flag is change to true, then ** checked separately in byte code. If the omit flag is changed to true, then
** the constraint may or may not be checked in byte code. In other words, ** the constraint may or may not be checked in byte code. In other words,
** when the omit flag is true there is no guarantee that the constraint will ** when the omit flag is true there is no guarantee that the constraint will
** not be checked again using byte code.)^ ** not be checked again using byte code.)^
@ -7568,7 +7637,7 @@ struct sqlite3_module {
** The xBestIndex method may optionally populate the idxFlags field with a ** The xBestIndex method may optionally populate the idxFlags field with a
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is ** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN] ** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
** output to show the idxNum has hex instead of as decimal. Another flag is ** output to show the idxNum as hex instead of as decimal. Another flag is
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will ** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
** return at most one row. ** return at most one row.
** **
@ -7709,7 +7778,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth ** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through ** parameter is an arbitrary client data pointer that is passed through
** into the [xCreate] and [xConnect] methods of the virtual table module ** into the [xCreate] and [xConnect] methods of the virtual table module
** when a new virtual table is be being created or reinitialized. ** when a new virtual table is being created or reinitialized.
** **
** ^The sqlite3_create_module_v2() interface has a fifth parameter which ** ^The sqlite3_create_module_v2() interface has a fifth parameter which
** is a pointer to a destructor for the pClientData. ^SQLite will ** is a pointer to a destructor for the pClientData. ^SQLite will
@ -7874,7 +7943,7 @@ typedef struct sqlite3_blob sqlite3_blob;
** in *ppBlob. Otherwise an [error code] is returned and, unless the error ** in *ppBlob. Otherwise an [error code] is returned and, unless the error
** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided ** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided
** the API is not misused, it is always safe to call [sqlite3_blob_close()] ** the API is not misused, it is always safe to call [sqlite3_blob_close()]
** on *ppBlob after this function it returns. ** on *ppBlob after this function returns.
** **
** This function fails with SQLITE_ERROR if any of the following are true: ** This function fails with SQLITE_ERROR if any of the following are true:
** <ul> ** <ul>
@ -7994,7 +8063,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
** **
** ^Returns the size in bytes of the BLOB accessible via the ** ^Returns the size in bytes of the BLOB accessible via the
** successfully opened [BLOB handle] in its only argument. ^The ** successfully opened [BLOB handle] in its only argument. ^The
** incremental blob I/O routines can only read or overwriting existing ** incremental blob I/O routines can only read or overwrite existing
** blob content; they cannot change the size of a blob. ** blob content; they cannot change the size of a blob.
** **
** This routine only works on a [BLOB handle] which has been created ** This routine only works on a [BLOB handle] which has been created
@ -8144,7 +8213,7 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
** ^The sqlite3_mutex_alloc() routine allocates a new ** ^The sqlite3_mutex_alloc() routine allocates a new
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc() ** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** routine returns NULL if it is unable to allocate the requested ** routine returns NULL if it is unable to allocate the requested
** mutex. The argument to sqlite3_mutex_alloc() must one of these ** mutex. The argument to sqlite3_mutex_alloc() must be one of these
** integer constants: ** integer constants:
** **
** <ul> ** <ul>
@ -8377,7 +8446,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection ** CAPI3REF: Retrieve the mutex for a database connection
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^This interface returns a pointer the [sqlite3_mutex] object that ** ^This interface returns a pointer to the [sqlite3_mutex] object that
** serializes access to the [database connection] given in the argument ** serializes access to the [database connection] given in the argument
** when the [threading mode] is Serialized. ** when the [threading mode] is Serialized.
** ^If the [threading mode] is Single-thread or Multi-thread then this ** ^If the [threading mode] is Single-thread or Multi-thread then this
@ -8500,7 +8569,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** CAPI3REF: SQL Keyword Checking ** CAPI3REF: SQL Keyword Checking
** **
** These routines provide access to the set of SQL language keywords ** These routines provide access to the set of SQL language keywords
** recognized by SQLite. Applications can uses these routines to determine ** recognized by SQLite. Applications can use these routines to determine
** whether or not a specific identifier needs to be escaped (for example, ** whether or not a specific identifier needs to be escaped (for example,
** by enclosing in double-quotes) so as not to confuse the parser. ** by enclosing in double-quotes) so as not to confuse the parser.
** **
@ -8668,7 +8737,7 @@ SQLITE_API void sqlite3_str_reset(sqlite3_str*);
** content of the dynamic string under construction in X. The value ** content of the dynamic string under construction in X. The value
** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X ** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X
** and might be freed or altered by any subsequent method on the same ** and might be freed or altered by any subsequent method on the same
** [sqlite3_str] object. Applications must not used the pointer returned ** [sqlite3_str] object. Applications must not use the pointer returned by
** [sqlite3_str_value(X)] after any subsequent method call on the same ** [sqlite3_str_value(X)] after any subsequent method call on the same
** object. ^Applications may change the content of the string returned ** object. ^Applications may change the content of the string returned
** by [sqlite3_str_value(X)] as long as they do not write into any bytes ** by [sqlite3_str_value(X)] as long as they do not write into any bytes
@ -8754,7 +8823,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE] ** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The ** buffer and where forced to overflow to [sqlite3_malloc()]. The
** returned value includes allocations that overflowed because they ** returned value includes allocations that overflowed because they
** where too large (they were larger than the "sz" parameter to ** were too large (they were larger than the "sz" parameter to
** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because ** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because
** no space was left in the page cache.</dd>)^ ** no space was left in the page cache.</dd>)^
** **
@ -8838,28 +8907,29 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt> ** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt>
** <dd>This parameter returns the number of malloc attempts that were ** <dd>This parameter returns the number of malloc attempts that were
** satisfied using lookaside memory. Only the high-water value is meaningful; ** satisfied using lookaside memory. Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to the amount of ** been satisfied using lookaside memory but failed due to the amount of
** memory requested being larger than the lookaside slot size. ** memory requested being larger than the lookaside slot size.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]] ** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt> ** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt>
** <dd>This parameter returns the number malloc attempts that might have ** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to all lookaside ** been satisfied using lookaside memory but failed due to all lookaside
** memory already being in use. ** memory already being in use.
** Only the high-water value is meaningful; ** Only the high-water value is meaningful;
** the current value is always zero.)^ ** the current value is always zero.</dd>)^
** **
** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt> ** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^ ** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]] ** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt> ** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt>
@ -8868,10 +8938,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** memory used by that pager cache is divided evenly between the attached ** memory used by that pager cache is divided evenly between the attached
** connections.)^ In other words, if none of the pager caches associated ** connections.)^ In other words, if none of the pager caches associated
** with the database connection are shared, this request returns the same ** with the database connection are shared, this request returns the same
** value as DBSTATUS_CACHE_USED. Or, if one or more or the pager caches are ** value as DBSTATUS_CACHE_USED. Or, if one or more of the pager caches are
** shared, the value returned by this call will be smaller than that returned ** shared, the value returned by this call will be smaller than that returned
** by DBSTATUS_CACHE_USED. ^The highwater mark associated with ** by DBSTATUS_CACHE_USED. ^The highwater mark associated with
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0. ** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.</dd>
** **
** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt> ** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -8881,6 +8951,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** schema memory is shared with other database connections due to ** schema memory is shared with other database connections due to
** [shared cache mode] being enabled. ** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0. ** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
** **
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt> ** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap ** <dd>This parameter returns the approximate number of bytes of heap
@ -8917,7 +8988,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** been written to disk in the middle of a transaction due to the page ** been written to disk in the middle of a transaction due to the page
** cache overflowing. Transactions are more efficient if they are written ** cache overflowing. Transactions are more efficient if they are written
** to disk all at once. When pages spill mid-transaction, that introduces ** to disk all at once. When pages spill mid-transaction, that introduces
** additional overhead. This parameter can be used help identify ** additional overhead. This parameter can be used to help identify
** inefficiencies that can be resolved by increasing the cache size. ** inefficiencies that can be resolved by increasing the cache size.
** </dd> ** </dd>
** **
@ -8988,13 +9059,13 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt> ** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt>
** <dd>^This is the number of sort operations that have occurred. ** <dd>^This is the number of sort operations that have occurred.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance through careful use of indices.</dd> ** improve performance through careful use of indices.</dd>
** **
** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt> ** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt>
** <dd>^This is the number of rows inserted into transient indices that ** <dd>^This is the number of rows inserted into transient indices that
** were created automatically in order to help joins run faster. ** were created automatically in order to help joins run faster.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance by adding permanent indices that do not ** improve performance by adding permanent indices that do not
** need to be reinitialized each time the statement is run.</dd> ** need to be reinitialized each time the statement is run.</dd>
** **
** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt> ** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt>
@ -9003,19 +9074,19 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** to 2147483647. The number of virtual machine operations can be ** to 2147483647. The number of virtual machine operations can be
** used as a proxy for the total work done by the prepared statement. ** used as a proxy for the total work done by the prepared statement.
** If the number of virtual machine operations exceeds 2147483647 ** If the number of virtual machine operations exceeds 2147483647
** then the value returned by this statement status code is undefined. ** then the value returned by this statement status code is undefined.</dd>
** **
** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt> ** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt>
** <dd>^This is the number of times that the prepare statement has been ** <dd>^This is the number of times that the prepare statement has been
** automatically regenerated due to schema changes or changes to ** automatically regenerated due to schema changes or changes to
** [bound parameters] that might affect the query plan. ** [bound parameters] that might affect the query plan.</dd>
** **
** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt> ** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt>
** <dd>^This is the number of times that the prepared statement has ** <dd>^This is the number of times that the prepared statement has
** been run. A single "run" for the purposes of this counter is one ** been run. A single "run" for the purposes of this counter is one
** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()]. ** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()].
** The counter is incremented on the first [sqlite3_step()] call of each ** The counter is incremented on the first [sqlite3_step()] call of each
** cycle. ** cycle.</dd>
** **
** [[SQLITE_STMTSTATUS_FILTER_MISS]] ** [[SQLITE_STMTSTATUS_FILTER_MISS]]
** [[SQLITE_STMTSTATUS_FILTER HIT]] ** [[SQLITE_STMTSTATUS_FILTER HIT]]
@ -9025,7 +9096,7 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** step was bypassed because a Bloom filter returned not-found. The ** step was bypassed because a Bloom filter returned not-found. The
** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of ** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of
** times that the Bloom filter returned a find, and thus the join step ** times that the Bloom filter returned a find, and thus the join step
** had to be processed as normal. ** had to be processed as normal.</dd>
** **
** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt> ** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt>
** <dd>^This is the approximate number of bytes of heap memory ** <dd>^This is the approximate number of bytes of heap memory
@ -9130,9 +9201,9 @@ struct sqlite3_pcache_page {
** SQLite will typically create one cache instance for each open database file, ** SQLite will typically create one cache instance for each open database file,
** though this is not guaranteed. ^The ** though this is not guaranteed. ^The
** first parameter, szPage, is the size in bytes of the pages that must ** first parameter, szPage, is the size in bytes of the pages that must
** be allocated by the cache. ^szPage will always a power of two. ^The ** be allocated by the cache. ^szPage will always be a power of two. ^The
** second parameter szExtra is a number of bytes of extra storage ** second parameter szExtra is a number of bytes of extra storage
** associated with each page cache entry. ^The szExtra parameter will ** associated with each page cache entry. ^The szExtra parameter will be
** a number less than 250. SQLite will use the ** a number less than 250. SQLite will use the
** extra szExtra bytes on each page to store metadata about the underlying ** extra szExtra bytes on each page to store metadata about the underlying
** database page on disk. The value passed into szExtra depends ** database page on disk. The value passed into szExtra depends
@ -9140,17 +9211,17 @@ struct sqlite3_pcache_page {
** ^The third argument to xCreate(), bPurgeable, is true if the cache being ** ^The third argument to xCreate(), bPurgeable, is true if the cache being
** created will be used to cache database pages of a file stored on disk, or ** created will be used to cache database pages of a file stored on disk, or
** false if it is used for an in-memory database. The cache implementation ** false if it is used for an in-memory database. The cache implementation
** does not have to do anything special based with the value of bPurgeable; ** does not have to do anything special based upon the value of bPurgeable;
** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will ** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will
** never invoke xUnpin() except to deliberately delete a page. ** never invoke xUnpin() except to deliberately delete a page.
** ^In other words, calls to xUnpin() on a cache with bPurgeable set to ** ^In other words, calls to xUnpin() on a cache with bPurgeable set to
** false will always have the "discard" flag set to true. ** false will always have the "discard" flag set to true.
** ^Hence, a cache created with bPurgeable false will ** ^Hence, a cache created with bPurgeable set to false will
** never contain any unpinned pages. ** never contain any unpinned pages.
** **
** [[the xCachesize() page cache method]] ** [[the xCachesize() page cache method]]
** ^(The xCachesize() method may be called at any time by SQLite to set the ** ^(The xCachesize() method may be called at any time by SQLite to set the
** suggested maximum cache-size (number of pages stored by) the cache ** suggested maximum cache-size (number of pages stored) for the cache
** instance passed as the first argument. This is the value configured using ** instance passed as the first argument. This is the value configured using
** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable ** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable
** parameter, the implementation is not required to do anything with this ** parameter, the implementation is not required to do anything with this
@ -9177,12 +9248,12 @@ struct sqlite3_pcache_page {
** implementation must return a pointer to the page buffer with its content ** implementation must return a pointer to the page buffer with its content
** intact. If the requested page is not already in the cache, then the ** intact. If the requested page is not already in the cache, then the
** cache implementation should use the value of the createFlag ** cache implementation should use the value of the createFlag
** parameter to help it determined what action to take: ** parameter to help it determine what action to take:
** **
** <table border=1 width=85% align=center> ** <table border=1 width=85% align=center>
** <tr><th> createFlag <th> Behavior when page is not already in cache ** <tr><th> createFlag <th> Behavior when page is not already in cache
** <tr><td> 0 <td> Do not allocate a new page. Return NULL. ** <tr><td> 0 <td> Do not allocate a new page. Return NULL.
** <tr><td> 1 <td> Allocate a new page if it easy and convenient to do so. ** <tr><td> 1 <td> Allocate a new page if it is easy and convenient to do so.
** Otherwise return NULL. ** Otherwise return NULL.
** <tr><td> 2 <td> Make every effort to allocate a new page. Only return ** <tr><td> 2 <td> Make every effort to allocate a new page. Only return
** NULL if allocating a new page is effectively impossible. ** NULL if allocating a new page is effectively impossible.
@ -9199,7 +9270,7 @@ struct sqlite3_pcache_page {
** as its second argument. If the third parameter, discard, is non-zero, ** as its second argument. If the third parameter, discard, is non-zero,
** then the page must be evicted from the cache. ** then the page must be evicted from the cache.
** ^If the discard parameter is ** ^If the discard parameter is
** zero, then the page may be discarded or retained at the discretion of ** zero, then the page may be discarded or retained at the discretion of the
** page cache implementation. ^The page cache implementation ** page cache implementation. ^The page cache implementation
** may choose to evict unpinned pages at any time. ** may choose to evict unpinned pages at any time.
** **
@ -9217,7 +9288,7 @@ struct sqlite3_pcache_page {
** When SQLite calls the xTruncate() method, the cache must discard all ** When SQLite calls the xTruncate() method, the cache must discard all
** existing cache entries with page numbers (keys) greater than or equal ** existing cache entries with page numbers (keys) greater than or equal
** to the value of the iLimit parameter passed to xTruncate(). If any ** to the value of the iLimit parameter passed to xTruncate(). If any
** of these pages are pinned, they are implicitly unpinned, meaning that ** of these pages are pinned, they become implicitly unpinned, meaning that
** they can be safely discarded. ** they can be safely discarded.
** **
** [[the xDestroy() page cache method]] ** [[the xDestroy() page cache method]]
@ -9397,7 +9468,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being ** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically ** used by the backup operation, then the backup will be automatically
** restarted by the next call to sqlite3_backup_step(). ^If the source ** restarted by the next call to sqlite3_backup_step(). ^If the source
** database is modified by the using the same database connection as is used ** database is modified by using the same database connection as is used
** by the backup operation, then the backup database is automatically ** by the backup operation, then the backup database is automatically
** updated at the same time. ** updated at the same time.
** **
@ -9414,7 +9485,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** and may not be used following a call to sqlite3_backup_finish(). ** and may not be used following a call to sqlite3_backup_finish().
** **
** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no ** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no
** sqlite3_backup_step() errors occurred, regardless or whether or not ** sqlite3_backup_step() errors occurred, regardless of whether or not
** sqlite3_backup_step() completed. ** sqlite3_backup_step() completed.
** ^If an out-of-memory condition or IO error occurred during any prior ** ^If an out-of-memory condition or IO error occurred during any prior
** sqlite3_backup_step() call on the same [sqlite3_backup] object, then ** sqlite3_backup_step() call on the same [sqlite3_backup] object, then
@ -9516,7 +9587,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** application receives an SQLITE_LOCKED error, it may call the ** application receives an SQLITE_LOCKED error, it may call the
** sqlite3_unlock_notify() method with the blocked connection handle as ** sqlite3_unlock_notify() method with the blocked connection handle as
** the first argument to register for a callback that will be invoked ** the first argument to register for a callback that will be invoked
** when the blocking connections current transaction is concluded. ^The ** when the blocking connection's current transaction is concluded. ^The
** callback is invoked from within the [sqlite3_step] or [sqlite3_close] ** callback is invoked from within the [sqlite3_step] or [sqlite3_close]
** call that concludes the blocking connection's transaction. ** call that concludes the blocking connection's transaction.
** **
@ -9536,7 +9607,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** blocked connection already has a registered unlock-notify callback, ** blocked connection already has a registered unlock-notify callback,
** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is ** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is
** called with a NULL pointer as its second argument, then any existing ** called with a NULL pointer as its second argument, then any existing
** unlock-notify callback is canceled. ^The blocked connections ** unlock-notify callback is canceled. ^The blocked connection's
** unlock-notify callback may also be canceled by closing the blocked ** unlock-notify callback may also be canceled by closing the blocked
** connection using [sqlite3_close()]. ** connection using [sqlite3_close()].
** **
@ -9934,7 +10005,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** support constraints. In this configuration (which is the default) if ** support constraints. In this configuration (which is the default) if
** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire ** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire
** statement is rolled back as if [ON CONFLICT | OR ABORT] had been ** statement is rolled back as if [ON CONFLICT | OR ABORT] had been
** specified as part of the users SQL statement, regardless of the actual ** specified as part of the user's SQL statement, regardless of the actual
** ON CONFLICT mode specified. ** ON CONFLICT mode specified.
** **
** If X is non-zero, then the virtual table implementation guarantees ** If X is non-zero, then the virtual table implementation guarantees
@ -9968,7 +10039,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt> ** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt>
** <dd>Calls of the form ** <dd>Calls of the form
** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the ** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the
** the [xConnect] or [xCreate] methods of a [virtual table] implementation ** [xConnect] or [xCreate] methods of a [virtual table] implementation
** identify that virtual table as being safe to use from within triggers ** identify that virtual table as being safe to use from within triggers
** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the ** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the
** virtual table can do no serious harm even if it is controlled by a ** virtual table can do no serious harm even if it is controlled by a
@ -10136,7 +10207,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** </table> ** </table>
** **
** ^For the purposes of comparing virtual table output values to see if the ** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered ** values are the same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS" ** to be the same. In other words, the comparison operator is "IS"
** (or "IS NOT DISTINCT FROM") and not "==". ** (or "IS NOT DISTINCT FROM") and not "==".
** **
@ -10146,7 +10217,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** **
** ^A virtual table implementation is always free to return rows in any order ** ^A virtual table implementation is always free to return rows in any order
** it wants, as long as the "orderByConsumed" flag is not set. ^When the ** it wants, as long as the "orderByConsumed" flag is not set. ^When the
** the "orderByConsumed" flag is unset, the query planner will add extra ** "orderByConsumed" flag is unset, the query planner will add extra
** [bytecode] to ensure that the final results returned by the SQL query are ** [bytecode] to ensure that the final results returned by the SQL query are
** ordered correctly. The use of the "orderByConsumed" flag and the ** ordered correctly. The use of the "orderByConsumed" flag and the
** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful ** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful
@ -10243,7 +10314,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** sqlite3_vtab_in_next(X,P) should be one of the parameters to the ** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
** xFilter method which invokes these routines, and specifically ** xFilter method which invokes these routines, and specifically
** a parameter that was previously selected for all-at-once IN constraint ** a parameter that was previously selected for all-at-once IN constraint
** processing use the [sqlite3_vtab_in()] interface in the ** processing using the [sqlite3_vtab_in()] interface in the
** [xBestIndex|xBestIndex method]. ^(If the X parameter is not ** [xBestIndex|xBestIndex method]. ^(If the X parameter is not
** an xFilter argument that was selected for all-at-once IN constraint ** an xFilter argument that was selected for all-at-once IN constraint
** processing, then these routines return [SQLITE_ERROR].)^ ** processing, then these routines return [SQLITE_ERROR].)^
@ -10298,7 +10369,7 @@ SQLITE_API int sqlite3_vtab_in_next(sqlite3_value *pVal, sqlite3_value **ppOut);
** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V) ** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V)
** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th ** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th
** constraint is not available. ^The sqlite3_vtab_rhs_value() interface ** constraint is not available. ^The sqlite3_vtab_rhs_value() interface
** can return an result code other than SQLITE_OK or SQLITE_NOTFOUND if ** can return a result code other than SQLITE_OK or SQLITE_NOTFOUND if
** something goes wrong. ** something goes wrong.
** **
** The sqlite3_vtab_rhs_value() interface is usually only successful if ** The sqlite3_vtab_rhs_value() interface is usually only successful if
@ -10326,8 +10397,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** KEYWORDS: {conflict resolution mode} ** KEYWORDS: {conflict resolution mode}
** **
** These constants are returned by [sqlite3_vtab_on_conflict()] to ** These constants are returned by [sqlite3_vtab_on_conflict()] to
** inform a [virtual table] implementation what the [ON CONFLICT] mode ** inform a [virtual table] implementation of the [ON CONFLICT] mode
** is for the SQL statement being evaluated. ** for the SQL statement being evaluated.
** **
** Note that the [SQLITE_IGNORE] constant is also used as a potential ** Note that the [SQLITE_IGNORE] constant is also used as a potential
** return value from the [sqlite3_set_authorizer()] callback and that ** return value from the [sqlite3_set_authorizer()] callback and that
@ -10367,39 +10438,39 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt> ** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt>
** <dd>^The "double" variable pointed to by the V parameter will be set to the ** <dd>^The "double" variable pointed to by the V parameter will be set to the
** query planner's estimate for the average number of rows output from each ** query planner's estimate for the average number of rows output from each
** iteration of the X-th loop. If the query planner's estimates was accurate, ** iteration of the X-th loop. If the query planner's estimate was accurate,
** then this value will approximate the quotient NVISIT/NLOOP and the ** then this value will approximate the quotient NVISIT/NLOOP and the
** product of this value for all prior loops with the same SELECTID will ** product of this value for all prior loops with the same SELECTID will
** be the NLOOP value for the current loop. ** be the NLOOP value for the current loop.</dd>
** **
** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt> ** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the name of the index or table ** to a zero-terminated UTF-8 string containing the name of the index or table
** used for the X-th loop. ** used for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt> ** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN] ** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
** description for the X-th loop. ** description for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt> ** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
** <dd>^The "int" variable pointed to by the V parameter will be set to the ** <dd>^The "int" variable pointed to by the V parameter will be set to the
** id for the X-th query plan element. The id value is unique within the ** id for the X-th query plan element. The id value is unique within the
** statement. The select-id is the same value as is output in the first ** statement. The select-id is the same value as is output in the first
** column of an [EXPLAIN QUERY PLAN] query. ** column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt> ** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
** <dd>The "int" variable pointed to by the V parameter will be set to the ** <dd>The "int" variable pointed to by the V parameter will be set to the
** the id of the parent of the current query element, if applicable, or ** id of the parent of the current query element, if applicable, or
** to zero if the query element has no parent. This is the same value as ** to zero if the query element has no parent. This is the same value as
** returned in the second column of an [EXPLAIN QUERY PLAN] query. ** returned in the second column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt> ** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
** <dd>The sqlite3_int64 output value is set to the number of cycles, ** <dd>The sqlite3_int64 output value is set to the number of cycles,
** according to the processor time-stamp counter, that elapsed while the ** according to the processor time-stamp counter, that elapsed while the
** query element was being processed. This value is not available for ** query element was being processed. This value is not available for
** all query elements - if it is unavailable the output variable is ** all query elements - if it is unavailable the output variable is
** set to -1. ** set to -1.</dd>
** </dl> ** </dl>
*/ */
#define SQLITE_SCANSTAT_NLOOP 0 #define SQLITE_SCANSTAT_NLOOP 0
@ -10440,8 +10511,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter. ** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
** **
** Parameter "idx" identifies the specific query element to retrieve statistics ** Parameter "idx" identifies the specific query element to retrieve statistics
** for. Query elements are numbered starting from zero. A value of -1 may be ** for. Query elements are numbered starting from zero. A value of -1 may
** to query for statistics regarding the entire query. ^If idx is out of range ** retrieve statistics for the entire query. ^If idx is out of range
** - less than -1 or greater than or equal to the total number of query ** - less than -1 or greater than or equal to the total number of query
** elements used to implement the statement - a non-zero value is returned and ** elements used to implement the statement - a non-zero value is returned and
** the variable that pOut points to is unchanged. ** the variable that pOut points to is unchanged.
@ -10484,7 +10555,7 @@ SQLITE_API void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*);
** METHOD: sqlite3 ** METHOD: sqlite3
** **
** ^If a write-transaction is open on [database connection] D when the ** ^If a write-transaction is open on [database connection] D when the
** [sqlite3_db_cacheflush(D)] interface invoked, any dirty ** [sqlite3_db_cacheflush(D)] interface is invoked, any dirty
** pages in the pager-cache that are not currently in use are written out ** pages in the pager-cache that are not currently in use are written out
** to disk. A dirty page may be in use if a database cursor created by an ** to disk. A dirty page may be in use if a database cursor created by an
** active SQL statement is reading from it, or if it is page 1 of a database ** active SQL statement is reading from it, or if it is page 1 of a database
@ -10598,8 +10669,8 @@ SQLITE_API int sqlite3_db_cacheflush(sqlite3*);
** triggers; and so forth. ** triggers; and so forth.
** **
** When the [sqlite3_blob_write()] API is used to update a blob column, ** When the [sqlite3_blob_write()] API is used to update a blob column,
** the pre-update hook is invoked with SQLITE_DELETE. This is because the ** the pre-update hook is invoked with SQLITE_DELETE, because
** in this case the new values are not available. In this case, when a ** the new values are not yet available. In this case, when a
** callback made with op==SQLITE_DELETE is actually a write using the ** callback made with op==SQLITE_DELETE is actually a write using the
** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns ** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns
** the index of the column being written. In other cases, where the ** the index of the column being written. In other cases, where the
@ -10852,7 +10923,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** For an ordinary on-disk database file, the serialization is just a ** For an ordinary on-disk database file, the serialization is just a
** copy of the disk file. For an in-memory database or a "TEMP" database, ** copy of the disk file. For an in-memory database or a "TEMP" database,
** the serialization is the same sequence of bytes which would be written ** the serialization is the same sequence of bytes which would be written
** to disk if that database where backed up to disk. ** to disk if that database were backed up to disk.
** **
** The usual case is that sqlite3_serialize() copies the serialization of ** The usual case is that sqlite3_serialize() copies the serialization of
** the database into memory obtained from [sqlite3_malloc64()] and returns ** the database into memory obtained from [sqlite3_malloc64()] and returns
@ -10861,7 +10932,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations ** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations
** are made, and the sqlite3_serialize() function will return a pointer ** are made, and the sqlite3_serialize() function will return a pointer
** to the contiguous memory representation of the database that SQLite ** to the contiguous memory representation of the database that SQLite
** is currently using for that database, or NULL if the no such contiguous ** is currently using for that database, or NULL if no such contiguous
** memory representation of the database exists. A contiguous memory ** memory representation of the database exists. A contiguous memory
** representation of the database will usually only exist if there has ** representation of the database will usually only exist if there has
** been a prior call to [sqlite3_deserialize(D,S,...)] with the same ** been a prior call to [sqlite3_deserialize(D,S,...)] with the same
@ -10932,7 +11003,7 @@ SQLITE_API unsigned char *sqlite3_serialize(
** database is currently in a read transaction or is involved in a backup ** database is currently in a read transaction or is involved in a backup
** operation. ** operation.
** **
** It is not possible to deserialized into the TEMP database. If the ** It is not possible to deserialize into the TEMP database. If the
** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the ** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the
** function returns SQLITE_ERROR. ** function returns SQLITE_ERROR.
** **
@ -10954,7 +11025,7 @@ SQLITE_API int sqlite3_deserialize(
sqlite3 *db, /* The database connection */ sqlite3 *db, /* The database connection */
const char *zSchema, /* Which DB to reopen with the deserialization */ const char *zSchema, /* Which DB to reopen with the deserialization */
unsigned char *pData, /* The serialized database content */ unsigned char *pData, /* The serialized database content */
sqlite3_int64 szDb, /* Number bytes in the deserialization */ sqlite3_int64 szDb, /* Number of bytes in the deserialization */
sqlite3_int64 szBuf, /* Total size of buffer pData[] */ sqlite3_int64 szBuf, /* Total size of buffer pData[] */
unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */ unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */
); );
@ -10962,7 +11033,7 @@ SQLITE_API int sqlite3_deserialize(
/* /*
** CAPI3REF: Flags for sqlite3_deserialize() ** CAPI3REF: Flags for sqlite3_deserialize()
** **
** The following are allowed values for 6th argument (the F argument) to ** The following are allowed values for the 6th argument (the F argument) to
** the [sqlite3_deserialize(D,S,P,N,M,F)] interface. ** the [sqlite3_deserialize(D,S,P,N,M,F)] interface.
** **
** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization ** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization
@ -11487,9 +11558,10 @@ SQLITE_API void sqlite3session_table_filter(
** is inserted while a session object is enabled, then later deleted while ** is inserted while a session object is enabled, then later deleted while
** the same session object is disabled, no INSERT record will appear in the ** the same session object is disabled, no INSERT record will appear in the
** changeset, even though the delete took place while the session was disabled. ** changeset, even though the delete took place while the session was disabled.
** Or, if one field of a row is updated while a session is disabled, and ** Or, if one field of a row is updated while a session is enabled, and
** another field of the same row is updated while the session is enabled, the ** then another field of the same row is updated while the session is disabled,
** resulting changeset will contain an UPDATE change that updates both fields. ** the resulting changeset will contain an UPDATE change that updates both
** fields.
*/ */
SQLITE_API int sqlite3session_changeset( SQLITE_API int sqlite3session_changeset(
sqlite3_session *pSession, /* Session object */ sqlite3_session *pSession, /* Session object */
@ -11561,8 +11633,9 @@ SQLITE_API sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession
** database zFrom the contents of the two compatible tables would be ** database zFrom the contents of the two compatible tables would be
** identical. ** identical.
** **
** It an error if database zFrom does not exist or does not contain the ** Unless the call to this function is a no-op as described above, it is an
** required compatible table. ** error if database zFrom does not exist or does not contain the required
** compatible table.
** **
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
@ -11697,7 +11770,7 @@ SQLITE_API int sqlite3changeset_start_v2(
** The following flags may passed via the 4th parameter to ** The following flags may passed via the 4th parameter to
** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]: ** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]:
** **
** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd> ** <dt>SQLITE_CHANGESETSTART_INVERT <dd>
** Invert the changeset while iterating through it. This is equivalent to ** Invert the changeset while iterating through it. This is equivalent to
** inverting a changeset using sqlite3changeset_invert() before applying it. ** inverting a changeset using sqlite3changeset_invert() before applying it.
** It is an error to specify this flag with a patchset. ** It is an error to specify this flag with a patchset.
@ -12012,19 +12085,6 @@ SQLITE_API int sqlite3changeset_concat(
void **ppOut /* OUT: Buffer containing output changeset */ void **ppOut /* OUT: Buffer containing output changeset */
); );
/*
** CAPI3REF: Upgrade the Schema of a Changeset/Patchset
*/
SQLITE_API int sqlite3changeset_upgrade(
sqlite3 *db,
const char *zDb,
int nIn, const void *pIn, /* Input changeset */
int *pnOut, void **ppOut /* OUT: Inverse of input */
);
/* /*
** CAPI3REF: Changegroup Handle ** CAPI3REF: Changegroup Handle
** **

View file

@ -16,53 +16,10 @@ package sqlite3
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif
#include <stdlib.h>
static int
_sqlite3_user_authenticate(sqlite3* db, const char* zUsername, const char* aPW, int nPW)
{
return sqlite3_user_authenticate(db, zUsername, aPW, nPW);
}
static int
_sqlite3_user_add(sqlite3* db, const char* zUsername, const char* aPW, int nPW, int isAdmin)
{
return sqlite3_user_add(db, zUsername, aPW, nPW, isAdmin);
}
static int
_sqlite3_user_change(sqlite3* db, const char* zUsername, const char* aPW, int nPW, int isAdmin)
{
return sqlite3_user_change(db, zUsername, aPW, nPW, isAdmin);
}
static int
_sqlite3_user_delete(sqlite3* db, const char* zUsername)
{
return sqlite3_user_delete(db, zUsername);
}
static int
_sqlite3_auth_enabled(sqlite3* db)
{
int exists = -1;
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "select count(type) from sqlite_master WHERE type='table' and name='sqlite_user';", -1, &stmt, NULL);
while ( sqlite3_step(stmt) == SQLITE_ROW) {
exists = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return exists;
}
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"unsafe"
) )
const ( const (
@ -70,8 +27,9 @@ const (
) )
var ( var (
ErrUnauthorized = errors.New("SQLITE_AUTH: Unauthorized") ErrUnauthorized = errors.New("SQLITE_AUTH: Unauthorized")
ErrAdminRequired = errors.New("SQLITE_AUTH: Unauthorized; Admin Privileges Required") ErrAdminRequired = errors.New("SQLITE_AUTH: Unauthorized; Admin Privileges Required")
errUserAuthNoLongerSupported = errors.New("sqlite3: the sqlite_userauth tag is no longer supported as the userauth extension is no longer supported by the SQLite authors, see https://github.com/mattn/go-sqlite3/issues/1341")
) )
// Authenticate will perform an authentication of the provided username // Authenticate will perform an authentication of the provided username
@ -88,15 +46,7 @@ var (
// If the SQLITE_USER table is not present in the database file, then // If the SQLITE_USER table is not present in the database file, then
// this interface is a harmless no-op returning SQLITE_OK. // this interface is a harmless no-op returning SQLITE_OK.
func (c *SQLiteConn) Authenticate(username, password string) error { func (c *SQLiteConn) Authenticate(username, password string) error {
rv := c.authenticate(username, password) return errUserAuthNoLongerSupported
switch rv {
case C.SQLITE_ERROR, C.SQLITE_AUTH:
return ErrUnauthorized
case C.SQLITE_OK:
return nil
default:
return c.lastError()
}
} }
// authenticate provides the actual authentication to SQLite. // authenticate provides the actual authentication to SQLite.
@ -109,17 +59,7 @@ func (c *SQLiteConn) Authenticate(username, password string) error {
// C.SQLITE_ERROR (1) // C.SQLITE_ERROR (1)
// C.SQLITE_AUTH (23) // C.SQLITE_AUTH (23)
func (c *SQLiteConn) authenticate(username, password string) int { func (c *SQLiteConn) authenticate(username, password string) int {
// Allocate C Variables return 1
cuser := C.CString(username)
cpass := C.CString(password)
// Free C Variables
defer func() {
C.free(unsafe.Pointer(cuser))
C.free(unsafe.Pointer(cpass))
}()
return int(C._sqlite3_user_authenticate(c.db, cuser, cpass, C.int(len(password))))
} }
// AuthUserAdd can be used (by an admin user only) // AuthUserAdd can be used (by an admin user only)
@ -131,20 +71,7 @@ func (c *SQLiteConn) authenticate(username, password string) int {
// for any ATTACH-ed databases. Any call to AuthUserAdd by a // for any ATTACH-ed databases. Any call to AuthUserAdd by a
// non-admin user results in an error. // non-admin user results in an error.
func (c *SQLiteConn) AuthUserAdd(username, password string, admin bool) error { func (c *SQLiteConn) AuthUserAdd(username, password string, admin bool) error {
isAdmin := 0 return errUserAuthNoLongerSupported
if admin {
isAdmin = 1
}
rv := c.authUserAdd(username, password, isAdmin)
switch rv {
case C.SQLITE_ERROR, C.SQLITE_AUTH:
return ErrAdminRequired
case C.SQLITE_OK:
return nil
default:
return c.lastError()
}
} }
// authUserAdd enables the User Authentication if not enabled. // authUserAdd enables the User Authentication if not enabled.
@ -162,17 +89,7 @@ func (c *SQLiteConn) AuthUserAdd(username, password string, admin bool) error {
// C.SQLITE_ERROR (1) // C.SQLITE_ERROR (1)
// C.SQLITE_AUTH (23) // C.SQLITE_AUTH (23)
func (c *SQLiteConn) authUserAdd(username, password string, admin int) int { func (c *SQLiteConn) authUserAdd(username, password string, admin int) int {
// Allocate C Variables return 1
cuser := C.CString(username)
cpass := C.CString(password)
// Free C Variables
defer func() {
C.free(unsafe.Pointer(cuser))
C.free(unsafe.Pointer(cpass))
}()
return int(C._sqlite3_user_add(c.db, cuser, cpass, C.int(len(password)), C.int(admin)))
} }
// AuthUserChange can be used to change a users // AuthUserChange can be used to change a users
@ -181,20 +98,7 @@ func (c *SQLiteConn) authUserAdd(username, password string, admin int) int {
// credentials or admin privilege setting. No user may change their own // credentials or admin privilege setting. No user may change their own
// admin privilege setting. // admin privilege setting.
func (c *SQLiteConn) AuthUserChange(username, password string, admin bool) error { func (c *SQLiteConn) AuthUserChange(username, password string, admin bool) error {
isAdmin := 0 return errUserAuthNoLongerSupported
if admin {
isAdmin = 1
}
rv := c.authUserChange(username, password, isAdmin)
switch rv {
case C.SQLITE_ERROR, C.SQLITE_AUTH:
return ErrAdminRequired
case C.SQLITE_OK:
return nil
default:
return c.lastError()
}
} }
// authUserChange allows to modify a user. // authUserChange allows to modify a user.
@ -215,17 +119,7 @@ func (c *SQLiteConn) AuthUserChange(username, password string, admin bool) error
// C.SQLITE_ERROR (1) // C.SQLITE_ERROR (1)
// C.SQLITE_AUTH (23) // C.SQLITE_AUTH (23)
func (c *SQLiteConn) authUserChange(username, password string, admin int) int { func (c *SQLiteConn) authUserChange(username, password string, admin int) int {
// Allocate C Variables return 1
cuser := C.CString(username)
cpass := C.CString(password)
// Free C Variables
defer func() {
C.free(unsafe.Pointer(cuser))
C.free(unsafe.Pointer(cpass))
}()
return int(C._sqlite3_user_change(c.db, cuser, cpass, C.int(len(password)), C.int(admin)))
} }
// AuthUserDelete can be used (by an admin user only) // AuthUserDelete can be used (by an admin user only)
@ -234,15 +128,7 @@ func (c *SQLiteConn) authUserChange(username, password string, admin int) int {
// the database cannot be converted into a no-authentication-required // the database cannot be converted into a no-authentication-required
// database. // database.
func (c *SQLiteConn) AuthUserDelete(username string) error { func (c *SQLiteConn) AuthUserDelete(username string) error {
rv := c.authUserDelete(username) return errUserAuthNoLongerSupported
switch rv {
case C.SQLITE_ERROR, C.SQLITE_AUTH:
return ErrAdminRequired
case C.SQLITE_OK:
return nil
default:
return c.lastError()
}
} }
// authUserDelete can be used to delete a user. // authUserDelete can be used to delete a user.
@ -258,25 +144,12 @@ func (c *SQLiteConn) AuthUserDelete(username string) error {
// C.SQLITE_ERROR (1) // C.SQLITE_ERROR (1)
// C.SQLITE_AUTH (23) // C.SQLITE_AUTH (23)
func (c *SQLiteConn) authUserDelete(username string) int { func (c *SQLiteConn) authUserDelete(username string) int {
// Allocate C Variables return 1
cuser := C.CString(username)
// Free C Variables
defer func() {
C.free(unsafe.Pointer(cuser))
}()
return int(C._sqlite3_user_delete(c.db, cuser))
} }
// AuthEnabled checks if the database is protected by user authentication // AuthEnabled checks if the database is protected by user authentication
func (c *SQLiteConn) AuthEnabled() (exists bool) { func (c *SQLiteConn) AuthEnabled() (exists bool) {
rv := c.authEnabled() return false
if rv == 1 {
exists = true
}
return
} }
// authEnabled perform the actual check for user authentication. // authEnabled perform the actual check for user authentication.
@ -289,7 +162,7 @@ func (c *SQLiteConn) AuthEnabled() (exists bool) {
// 0 - Disabled // 0 - Disabled
// 1 - Enabled // 1 - Enabled
func (c *SQLiteConn) authEnabled() int { func (c *SQLiteConn) authEnabled() int {
return int(C._sqlite3_auth_enabled(c.db)) return 0
} }
// EOF // EOF

View file

@ -371,6 +371,8 @@ struct sqlite3_api_routines {
/* Version 3.44.0 and later */ /* Version 3.44.0 and later */
void *(*get_clientdata)(sqlite3*,const char*); void *(*get_clientdata)(sqlite3*,const char*);
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*)); int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
/* Version 3.50.0 and later */
int (*setlk_timeout)(sqlite3*,int,int);
}; };
/* /*
@ -704,6 +706,8 @@ typedef int (*sqlite3_loadext_entry)(
/* Version 3.44.0 and later */ /* Version 3.44.0 and later */
#define sqlite3_get_clientdata sqlite3_api->get_clientdata #define sqlite3_get_clientdata sqlite3_api->get_clientdata
#define sqlite3_set_clientdata sqlite3_api->set_clientdata #define sqlite3_set_clientdata sqlite3_api->set_clientdata
/* Version 3.50.0 and later */
#define sqlite3_setlk_timeout sqlite3_api->setlk_timeout
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)

3
vendor/github.com/olekukonko/cat/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
.idea
.github
lab

21
vendor/github.com/olekukonko/cat/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleku Konko
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.

168
vendor/github.com/olekukonko/cat/README.md generated vendored Normal file
View file

@ -0,0 +1,168 @@
# 🐱 `cat` - The Fast & Fluent String Concatenation Library for Go
> **"Because building strings shouldn't feel like herding cats"** 😼
## Why `cat`?
Go's `strings.Builder` is great, but building complex strings often feels clunky. `cat` makes string concatenation:
- **Faster** - Optimized paths for common types, zero-allocation conversions
- **Fluent** - Chainable methods for beautiful, readable code
- **Flexible** - Handles any type, nested structures, and custom formatting
- **Smart** - Automatic pooling, size estimation, and separator handling
```go
// Without cat
var b strings.Builder
b.WriteString("Hello, ")
b.WriteString(user.Name)
b.WriteString("! You have ")
b.WriteString(strconv.Itoa(count))
b.WriteString(" new messages.")
result := b.String()
// With cat
result := cat.Concat("Hello, ", user.Name, "! You have ", count, " new messages.")
```
## 🔥 Hot Features
### 1. Fluent Builder API
Build strings like a boss with method chaining:
```go
s := cat.New(", ").
Add("apple").
If(user.IsVIP, "golden kiwi").
Add("orange").
Sep(" | "). // Change separator mid-way
Add("banana").
String()
// "apple, golden kiwi, orange | banana"
```
### 2. Zero-Allocation Magic
- **Pooled builders** (optional) reduce GC pressure
- **Unsafe byte conversions** (opt-in) avoid `[]byte`→`string` copies
- **Stack buffers** for numbers instead of heap allocations
```go
// Enable performance features
cat.Pool(true) // Builder pooling
cat.SetUnsafeBytes(true) // Zero-copy []byte conversion
```
### 3. Handles Any Type - Even Nested Ones!
No more manual type conversions:
```go
data := map[string]any{
"id": 12345,
"tags": []string{"go", "fast", "efficient"},
}
fmt.Println(cat.JSONPretty(data))
// {
// "id": 12345,
// "tags": ["go", "fast", "efficient"]
// }
```
### 4. Concatenation for Every Use Case
```go
// Simple joins
cat.With(", ", "apple", "banana", "cherry") // "apple, banana, cherry"
// File paths
cat.Path("dir", "sub", "file.txt") // "dir/sub/file.txt"
// CSV
cat.CSV(1, 2, 3) // "1,2,3"
// Conditional elements
cat.Start("Hello").If(user != nil, " ", user.Name) // "Hello" or "Hello Alice"
// Repeated patterns
cat.RepeatWith("-+", "X", 3) // "X-+X-+X"
```
### 5. Smarter Than Your Average String Lib
```go
// Automatic nesting handling
nested := []any{"a", []any{"b", "c"}, "d"}
cat.FlattenWith(",", nested) // "a,b,c,d"
// Precise size estimation (minimizes allocations)
b := cat.New(", ").Grow(estimatedSize) // Preallocate exactly what you need
// Reflection support for any type
cat.Reflect(anyComplexStruct) // "{Field1:value Field2:[1 2 3]}"
```
## 🚀 Getting Started
```bash
go get github.com/your-repo/cat
```
```go
import "github.com/your-repo/cat"
func main() {
// Simple concatenation
msg := cat.Concat("User ", userID, " has ", count, " items")
// Pooled builder (for high-performance loops)
builder := cat.New(", ")
defer builder.Release() // Return to pool
result := builder.Add(items...).String()
}
```
## 🤔 Why Not Just Use...?
- `fmt.Sprintf` - Slow, many allocations
- `strings.Join` - Only works with strings
- `bytes.Buffer` - No separator support, manual type handling
- `string +` - Even worse performance, especially in loops
## 💡 Pro Tips
1. **Enable pooling** in high-throughput scenarios
2. **Preallocate** with `.Grow()` when you know the final size
3. Use **`If()`** for conditional elements in fluent chains
4. Try **`SetUnsafeBytes(true)`** if you can guarantee byte slices won't mutate
5. **Release builders** when pooling is enabled
## 🐱‍👤 Advanced Usage
```go
// Custom value formatting
type User struct {
Name string
Age int
}
func (u User) String() string {
return cat.With(" ", u.Name, cat.Wrap("(", u.Age, ")"))
}
// JSON-like output
func JSONPretty(v any) string {
return cat.WrapWith(",\n ", "{\n ", "\n}", prettyFields(v))
}
```
```text
/\_/\
( o.o ) > Concatenate with purr-fection!
> ^ <
```
**`cat`** - Because life's too short for ugly string building code. 😻

124
vendor/github.com/olekukonko/cat/builder.go generated vendored Normal file
View file

@ -0,0 +1,124 @@
package cat
import (
"strings"
)
// Builder is a fluent concatenation helper. It is safe for concurrent use by
// multiple goroutines only if each goroutine uses a distinct *Builder.
// If pooling is enabled via Pool(true), call Release() when done.
// The Builder uses an internal strings.Builder for efficient string concatenation
// and manages a separator that is inserted between added values.
// It supports chaining methods for a fluent API style.
type Builder struct {
buf strings.Builder
sep string
needsSep bool
}
// New begins a new Builder with a separator. If pooling is enabled,
// the Builder is reused and MUST be released with b.Release() when done.
// If sep is empty, uses DefaultSep().
// Optional initial arguments x are added immediately after creation.
// Pooling is controlled globally via Pool(true/false); when enabled, Builders
// are recycled to reduce allocations in high-throughput scenarios.
func New(sep string, x ...any) *Builder {
var b *Builder
if poolEnabled.Load() {
b = builderPool.Get().(*Builder)
b.buf.Reset()
b.sep = sep
b.needsSep = false
} else {
b = &Builder{sep: sep}
}
// Process initial arguments *after* the builder is prepared.
if len(x) > 0 {
b.Add(x...)
}
return b
}
// Start begins a new Builder with no separator (using an empty string as sep).
// It is a convenience function that wraps New(empty, x...), where empty is a constant empty string.
// This allows starting a concatenation without any separator between initial or subsequent additions.
// If pooling is enabled via Pool(true), the returned Builder MUST be released with b.Release() when done.
// Optional variadic arguments x are passed directly to New and added immediately after creation.
// Useful for fluent chains where no default separator is desired from the start.
func Start(x ...any) *Builder {
return New(empty, x...)
}
// Grow pre-sizes the internal buffer.
// This can be used to preallocate capacity based on an estimated total size,
// reducing reallocations during subsequent Add calls.
// It chains, returning the Builder for fluent use.
func (b *Builder) Grow(n int) *Builder { b.buf.Grow(n); return b }
// Add appends values to the builder.
// It inserts the current separator before each new value if needed (i.e., after the first addition).
// Values are converted to strings using the optimized write function, which handles
// common types efficiently without allocations where possible.
// Supports any number of arguments of any type.
// Chains, returning the Builder for fluent use.
func (b *Builder) Add(args ...any) *Builder {
for _, arg := range args {
if b.needsSep && b.sep != empty {
b.buf.WriteString(b.sep)
}
write(&b.buf, arg)
b.needsSep = true
}
return b
}
// If appends values to the builder only if the condition is true.
// Behaves like Add when condition is true; does nothing otherwise.
// Useful for conditional concatenation in chains.
// Chains, returning the Builder for fluent use.
func (b *Builder) If(condition bool, args ...any) *Builder {
if condition {
b.Add(args...)
}
return b
}
// Sep changes the separator for subsequent additions.
// Future Add calls will use this new separator.
// Does not affect already added content.
// If sep is empty, no separator will be added between future values.
// Chains, returning the Builder for fluent use.
func (b *Builder) Sep(sep string) *Builder { b.sep = sep; return b }
// String returns the concatenated result.
// This does not release the Builder; if pooling is enabled, call Release separately
// if you are done with the Builder.
// Can be called multiple times; the internal buffer remains unchanged.
func (b *Builder) String() string { return b.buf.String() }
// Output returns the concatenated result and releases the Builder if pooling is enabled.
// This is a convenience method to get the string and clean up in one call.
// After Output, the Builder should not be used further if pooled, as it may be recycled.
// If pooling is disabled, it behaves like String without release.
func (b *Builder) Output() string {
out := b.buf.String()
b.Release() // Release takes care of the poolEnabled check
return out
}
// Release returns the Builder to the pool if pooling is enabled.
// You should call this exactly once per New() when Pool(true) is active.
// Resets the internal state (buffer, separator, needsSep) before pooling to avoid
// retaining data or large allocations.
// If pooling is disabled, this is a no-op.
// Safe to call multiple times, but typically called once at the end of use.
func (b *Builder) Release() {
if poolEnabled.Load() {
// Avoid retaining large buffers.
b.buf.Reset()
b.sep = empty
b.needsSep = false
builderPool.Put(b)
}
}

117
vendor/github.com/olekukonko/cat/cat.go generated vendored Normal file
View file

@ -0,0 +1,117 @@
// Package cat provides efficient and flexible string concatenation utilities.
// It includes optimized functions for concatenating various types, builders for fluent chaining,
// and configuration options for defaults, pooling, and unsafe optimizations.
// The package aims to minimize allocations and improve performance in string building scenarios.
package cat
import (
"sync"
"sync/atomic"
)
// Constants used throughout the package for separators, defaults, and configuration.
// These include common string literals for separators, empty strings, and special representations,
// as well as limits like recursion depth. Defining them as constants allows for compile-time
// optimizations, readability, and consistent usage in functions like Space, Path, CSV, and reflection handlers.
// cat.go (updated constants section)
const (
empty = "" // Empty string constant, used for checks and defaults.
space = " " // Single space, default separator.
slash = "/" // Forward slash, for paths.
dot = "." // Period, for extensions or decimals.
comma = "," // Comma, for CSV or lists.
equal = "=" // Equals, for comparisons.
newline = "\n" // Newline, for multi-line strings.
// SQL-specific constants
and = "AND" // AND operator, for SQL conditions.
inOpen = " IN (" // Opening for SQL IN clause
inClose = ")" // Closing for SQL IN clause
asSQL = " AS " // SQL AS for aliasing
count = "COUNT(" // SQL COUNT function prefix
sum = "SUM(" // SQL SUM function prefix
avg = "AVG(" // SQL AVG function prefix
maxOpen = "MAX(" // SQL MAX function prefix
minOpen = "MIN(" // SQL MIN function prefix
caseSQL = "CASE " // SQL CASE keyword
when = "WHEN " // SQL WHEN clause
then = " THEN " // SQL THEN clause
elseSQL = " ELSE " // SQL ELSE clause
end = " END" // SQL END for CASE
countAll = "COUNT(*)" // SQL COUNT(*) for all rows
parenOpen = "(" // Opening parenthesis
parenClose = ")" // Closing parenthesis
maxRecursionDepth = 32 // Maximum recursion depth for nested structure handling.
nilString = "<nil>" // String representation for nil values.
unexportedString = "<?>" // Placeholder for unexported fields.
)
// Numeric is a generic constraint interface for numeric types.
// It includes all signed/unsigned integers and floats.
// Used in generic functions like Number and NumberWith to constrain to numbers.
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
}
// poolEnabled controls whether New() reuses Builder instances from a pool.
// Atomic.Bool for thread-safe toggle.
// When true, Builders from New must be Released to avoid leaks.
var poolEnabled atomic.Bool
// builderPool stores reusable *Builder to reduce GC pressure on hot paths.
// Uses sync.Pool for efficient allocation/reuse.
// New func creates a fresh &Builder when pool is empty.
var builderPool = sync.Pool{
New: func() any { return &Builder{} },
}
// Pool enables or disables Builder pooling for New()/Release().
// When enabled, you MUST call b.Release() after b.String() to return it.
// Thread-safe via atomic.Store.
// Enable for high-throughput scenarios to reduce allocations.
func Pool(enable bool) { poolEnabled.Store(enable) }
// unsafeBytesFlag controls zero-copy []byte -> string behavior via atomics.
// Int32 used for atomic operations: 1 = enabled, 0 = disabled.
// Affects bytesToString function for zero-copy conversions using unsafe.
var unsafeBytesFlag atomic.Int32 // 1 = true, 0 = false
// SetUnsafeBytes toggles zero-copy []byte -> string conversions globally.
// When enabled, bytesToString uses unsafe.String for zero-allocation conversion.
// Thread-safe via atomic.Store.
// Use with caution: assumes the byte slice is not modified after conversion.
// Compatible with Go 1.20+; fallback to string(bts) if disabled.
func SetUnsafeBytes(enable bool) {
if enable {
unsafeBytesFlag.Store(1)
} else {
unsafeBytesFlag.Store(0)
}
}
// IsUnsafeBytes reports whether zero-copy []byte -> string is enabled.
// Thread-safe via atomic.Load.
// Returns true if flag is 1, false otherwise.
// Useful for checking current configuration.
func IsUnsafeBytes() bool { return unsafeBytesFlag.Load() == 1 }
// deterministicMaps controls whether map keys are sorted for deterministic output in string conversions.
// It uses atomic.Bool for thread-safe access.
var deterministicMaps atomic.Bool
// SetDeterministicMaps controls whether map keys are sorted for deterministic output
// in reflection-based handling (e.g., in writeReflect for maps).
// When enabled, keys are sorted using a string-based comparison for consistent string representations.
// Thread-safe via atomic.Store.
// Useful for reproducible outputs in testing or logging.
func SetDeterministicMaps(enable bool) {
deterministicMaps.Store(enable)
}
// IsDeterministicMaps returns current map sorting setting.
// Thread-safe via atomic.Load.
// Returns true if deterministic sorting is enabled, false otherwise.
func IsDeterministicMaps() bool {
return deterministicMaps.Load()
}

590
vendor/github.com/olekukonko/cat/concat.go generated vendored Normal file
View file

@ -0,0 +1,590 @@
package cat
import (
"reflect"
"strings"
)
// Append appends args to dst and returns the grown slice.
// Callers can reuse dst across calls to amortize allocs.
// It uses an internal Builder for efficient concatenation of the args (no separators),
// then appends the result to the dst byte slice.
// Preallocates based on a size estimate to minimize reallocations.
// Benefits from Builder pooling if enabled.
// Useful for building byte slices incrementally without separators.
func Append(dst []byte, args ...any) []byte {
return AppendWith(empty, dst, args...)
}
// AppendWith appends args to dst and returns the grown slice.
// Callers can reuse dst across calls to amortize allocs.
// Similar to Append, but inserts the specified sep between each arg.
// Preallocates based on a size estimate including separators.
// Benefits from Builder pooling if enabled.
// Useful for building byte slices incrementally with custom separators.
func AppendWith(sep string, dst []byte, args ...any) []byte {
if len(args) == 0 {
return dst
}
b := New(sep)
b.Grow(estimateWith(sep, args))
b.Add(args...)
out := b.Output()
return append(dst, out...)
}
// AppendBytes joins byte slices without separators.
// Only for compatibility with low-level byte processing.
// Directly appends each []byte arg to dst without any conversion or separators.
// Efficient for pure byte concatenation; no allocations if dst has capacity.
// Returns the extended dst slice.
// Does not use Builder, as it's simple append operations.
func AppendBytes(dst []byte, args ...[]byte) []byte {
if len(args) == 0 {
return dst
}
for _, b := range args {
dst = append(dst, b...)
}
return dst
}
// AppendTo writes arguments to an existing strings.Builder.
// More efficient than creating new builders.
// Appends each arg to the provided strings.Builder using the optimized write function.
// No separators are added; for direct concatenation.
// Useful when you already have a strings.Builder and want to add more values efficiently.
// Does not use cat.Builder, as it appends to an existing strings.Builder.
func AppendTo(b *strings.Builder, args ...any) {
for _, arg := range args {
write(b, arg)
}
}
// AppendStrings writes strings to an existing strings.Builder.
// Directly writes each string arg to the provided strings.Builder.
// No type checks or conversions; assumes all args are strings.
// Efficient for appending known strings without separators.
// Does not use cat.Builder, as it appends to an existing strings.Builder.
func AppendStrings(b *strings.Builder, ss ...string) {
for _, s := range ss {
b.WriteString(s)
}
}
// Between concatenates values wrapped between x and y (no separator between args).
// Equivalent to BetweenWith with an empty separator.
func Between(x, y any, args ...any) string {
return BetweenWith(empty, x, y, args...)
}
// BetweenWith concatenates values wrapped between x and y, using sep between x, args, and y.
// Uses a pooled Builder if enabled; releases it after use.
// Equivalent to With(sep, x, args..., y).
func BetweenWith(sep string, x, y any, args ...any) string {
b := New(sep)
// Estimate size for all parts to avoid re-allocation.
b.Grow(estimate([]any{x, y}) + estimateWith(sep, args))
b.Add(x)
b.Add(args...)
b.Add(y)
return b.Output()
}
// CSV joins arguments with "," separators (no space).
// Convenience wrapper for With using a comma as separator.
// Useful for simple CSV string generation without spaces.
func CSV(args ...any) string { return With(comma, args...) }
// Comma joins arguments with ", " separators.
// Convenience wrapper for With using ", " as separator.
// Useful for human-readable lists with comma and space.
func Comma(args ...any) string { return With(comma+space, args...) }
// Concat concatenates any values (no separators).
// Usage: cat.Concat("a", 1, true) → "a1true"
// Equivalent to With with an empty separator.
func Concat(args ...any) string {
return With(empty, args...)
}
// ConcatWith concatenates any values with separator.
// Alias for With; joins args with the provided sep.
func ConcatWith(sep string, args ...any) string {
return With(sep, args...)
}
// Flatten joins nested values into a single concatenation using empty.
// Convenience for FlattenWith using empty.
func Flatten(args ...any) string {
return FlattenWith(empty, args...)
}
// FlattenWith joins nested values into a single concatenation with sep, avoiding
// intermediate slice allocations where possible.
// It recursively flattens any nested []any arguments, concatenating all leaf items
// with sep between them. Skips empty nested slices to avoid extra separators.
// Leaf items (non-slices) are converted using the optimized write function.
// Uses a pooled Builder if enabled; releases it after use.
// Preallocates based on a recursive estimate for efficiency.
// Example: FlattenWith(",", 1, []any{2, []any{3,4}}, 5) → "1,2,3,4,5"
func FlattenWith(sep string, args ...any) string {
if len(args) == 0 {
return empty
}
// Recursive estimate for preallocation.
totalSize := recursiveEstimate(sep, args)
b := New(sep)
b.Grow(totalSize)
recursiveAdd(b, args)
return b.Output()
}
// Group joins multiple groups with empty between groups (no intra-group separators).
// Convenience for GroupWith using empty.
func Group(groups ...[]any) string {
return GroupWith(empty, groups...)
}
// GroupWith joins multiple groups with a separator between groups (no intra-group separators).
// Concatenates each group internally without separators, then joins non-empty groups with sep.
// Preestimates total size for allocation; uses pooled Builder if enabled.
// Optimized for single group: direct Concat.
// Useful for grouping related items with inter-group separation.
func GroupWith(sep string, groups ...[]any) string {
if len(groups) == 0 {
return empty
}
if len(groups) == 1 {
return Concat(groups[0]...)
}
total := 0
nonEmpty := 0
for _, g := range groups {
if len(g) == 0 {
continue
}
if nonEmpty > 0 {
total += len(sep)
}
total += estimate(g)
nonEmpty++
}
b := New(empty)
b.Grow(total)
first := true
for _, g := range groups {
if len(g) == 0 {
continue
}
if !first && sep != empty {
b.buf.WriteString(sep)
}
first = false
for _, a := range g {
write(&b.buf, a)
}
}
return b.Output()
}
// Indent prefixes the concatenation of args with depth levels of two spaces per level.
// Example: Indent(2, "hello") => " hello"
// If depth <= 0, equivalent to Concat(args...).
// Uses " " repeated depth times as prefix, followed by concatenated args (no separators).
// Benefits from pooling via Concat.
func Indent(depth int, args ...any) string {
if depth <= 0 {
return Concat(args...)
}
prefix := strings.Repeat(" ", depth)
return Prefix(prefix, args...)
}
// Join joins strings (matches stdlib strings.Join behavior).
// Usage: cat.Join("a", "b") → "a b" (using empty)
// Joins the variadic string args with the current empty.
// Useful for compatibility with stdlib but using package default sep.
func Join(elems ...string) string {
return strings.Join(elems, empty)
}
// JoinWith joins strings with separator (variadic version).
// Directly uses strings.Join on the variadic string args with sep.
// Efficient for known strings; no conversions needed.
func JoinWith(sep string, elems ...string) string {
return strings.Join(elems, sep)
}
// Lines joins arguments with newline separators.
// Convenience for With using "\n" as separator.
// Useful for building multi-line strings.
func Lines(args ...any) string { return With(newline, args...) }
// Number concatenates numeric values without separators.
// Generic over Numeric types.
// Equivalent to NumberWith with empty sep.
func Number[T Numeric](a ...T) string {
return NumberWith(empty, a...)
}
// NumberWith concatenates numeric values with the provided separator.
// Generic over Numeric types.
// If no args, returns empty string.
// Uses pooled Builder if enabled, with rough growth estimate (8 bytes per item).
// Relies on valueToString for numeric conversion.
func NumberWith[T Numeric](sep string, a ...T) string {
if len(a) == 0 {
return empty
}
b := New(sep)
b.Grow(len(a) * 8)
for _, v := range a {
b.Add(v)
}
return b.Output()
}
// Path joins arguments with "/" separators.
// Convenience for With using "/" as separator.
// Useful for building file paths or URLs.
func Path(args ...any) string { return With(slash, args...) }
// Prefix concatenates with a prefix (no separator).
// Equivalent to PrefixWith with empty sep.
func Prefix(p any, args ...any) string {
return PrefixWith(empty, p, args...)
}
// PrefixWith concatenates with a prefix and separator.
// Adds p, then sep (if args present and sep not empty), then joins args with sep.
// Uses pooled Builder if enabled.
func PrefixWith(sep string, p any, args ...any) string {
b := New(sep)
b.Grow(estimateWith(sep, args) + estimate([]any{p}))
b.Add(p)
b.Add(args...)
return b.Output()
}
// PrefixEach applies the same prefix to each argument and joins the pairs with sep.
// Example: PrefixEach("pre-", ",", "a","b") => "pre-a,pre-b"
// Preestimates size including prefixes and seps.
// Uses pooled Builder if enabled; manually adds sep between pairs, no sep between p and a.
// Returns empty if no args.
func PrefixEach(p any, sep string, args ...any) string {
if len(args) == 0 {
return empty
}
pSize := estimate([]any{p})
total := len(sep)*(len(args)-1) + estimate(args) + pSize*len(args)
b := New(empty)
b.Grow(total)
for i, a := range args {
if i > 0 && sep != empty {
b.buf.WriteString(sep)
}
write(&b.buf, p)
write(&b.buf, a)
}
return b.Output()
}
// Pair joins exactly two values (no separator).
// Equivalent to PairWith with empty sep.
func Pair(a, b any) string {
return PairWith(empty, a, b)
}
// PairWith joins exactly two values with a separator.
// Optimized for two args: uses With(sep, a, b).
func PairWith(sep string, a, b any) string {
return With(sep, a, b)
}
// Quote wraps each argument in double quotes, separated by spaces.
// Equivalent to QuoteWith with '"' as quote.
func Quote(args ...any) string {
return QuoteWith('"', args...)
}
// QuoteWith wraps each argument with the specified quote byte, separated by spaces.
// Wraps each arg with quote, writes arg, closes with quote; joins with space.
// Preestimates with quotes and spaces.
// Uses pooled Builder if enabled.
func QuoteWith(quote byte, args ...any) string {
if len(args) == 0 {
return empty
}
total := estimate(args) + 2*len(args) + len(space)*(len(args)-1)
b := New(empty)
b.Grow(total)
need := false
for _, a := range args {
if need {
b.buf.WriteString(space)
}
b.buf.WriteByte(quote)
write(&b.buf, a)
b.buf.WriteByte(quote)
need = true
}
return b.Output()
}
// Repeat concatenates val n times (no sep between instances).
// Equivalent to RepeatWith with empty sep.
func Repeat(val any, n int) string {
return RepeatWith(empty, val, n)
}
// RepeatWith concatenates val n times with sep between each instance.
// If n <= 0, returns an empty string.
// Optimized to make exactly one allocation; converts val once.
// Uses pooled Builder if enabled.
func RepeatWith(sep string, val any, n int) string {
if n <= 0 {
return empty
}
if n == 1 {
return valueToString(val)
}
b := New(sep)
b.Grow(n*estimate([]any{val}) + (n-1)*len(sep))
for i := 0; i < n; i++ {
b.Add(val)
}
return b.Output()
}
// Reflect converts a reflect.Value to its string representation.
// It handles all kinds of reflected values including primitives, structs, slices, maps, etc.
// For nil values, it returns the nilString constant ("<nil>").
// For unexported or inaccessible fields, it returns unexportedString ("<?>").
// The output follows Go's syntax conventions where applicable (e.g., slices as [a, b], maps as {k:v}).
func Reflect(r reflect.Value) string {
if !r.IsValid() {
return nilString
}
var b strings.Builder
writeReflect(&b, r.Interface(), 0)
return b.String()
}
// Space concatenates arguments with space separators.
// Convenience for With using " " as separator.
func Space(args ...any) string { return With(space, args...) }
// Dot concatenates arguments with dot separators.
// Convenience for With using " " as separator.
func Dot(args ...any) string { return With(dot, args...) }
// Suffix concatenates with a suffix (no separator).
// Equivalent to SuffixWith with empty sep.
func Suffix(s any, args ...any) string {
return SuffixWith(empty, s, args...)
}
// SuffixWith concatenates with a suffix and separator.
// Joins args with sep, then adds sep (if args present and sep not empty), then s.
// Uses pooled Builder if enabled.
func SuffixWith(sep string, s any, args ...any) string {
b := New(sep)
b.Grow(estimateWith(sep, args) + estimate([]any{s}))
b.Add(args...)
b.Add(s)
return b.Output()
}
// SuffixEach applies the same suffix to each argument and joins the pairs with sep.
// Example: SuffixEach("-suf", " | ", "a","b") => "a-suf | b-suf"
// Preestimates size including suffixes and seps.
// Uses pooled Builder if enabled; manually adds sep between pairs, no sep between a and s.
// Returns empty if no args.
func SuffixEach(s any, sep string, args ...any) string {
if len(args) == 0 {
return empty
}
sSize := estimate([]any{s})
total := len(sep)*(len(args)-1) + estimate(args) + sSize*len(args)
b := New(empty)
b.Grow(total)
for i, a := range args {
if i > 0 && sep != empty {
b.buf.WriteString(sep)
}
write(&b.buf, a)
write(&b.buf, s)
}
return b.Output()
}
// Sprint concatenates any values (no separators).
// Usage: Sprint("a", 1, true) → "a1true"
// Equivalent to Concat or With with an empty separator.
func Sprint(args ...any) string {
if len(args) == 0 {
return empty
}
if len(args) == 1 {
return valueToString(args[0])
}
// For multiple args, use the existing Concat functionality
return Concat(args...)
}
// Trio joins exactly three values (no separator).
// Equivalent to TrioWith with empty sep
func Trio(a, b, c any) string {
return TrioWith(empty, a, b, c)
}
// TrioWith joins exactly three values with a separator.
// Optimized for three args: uses With(sep, a, b, c).
func TrioWith(sep string, a, b, c any) string {
return With(sep, a, b, c)
}
// With concatenates arguments with the specified separator.
// Core concatenation function with sep.
// Optimized for zero or one arg: empty or direct valueToString.
// Fast path for all strings: exact preallocation, direct writes via raw strings.Builder (minimal branches/allocs).
// Fallback: pooled Builder with estimateWith, adds args with sep.
// Benefits from pooling if enabled for mixed types.
func With(sep string, args ...any) string {
switch len(args) {
case 0:
return empty
case 1:
return valueToString(args[0])
}
// Fast path for all strings: use raw strings.Builder for speed, no pooling needed.
allStrings := true
totalLen := len(sep) * (len(args) - 1)
for _, a := range args {
if s, ok := a.(string); ok {
totalLen += len(s)
} else {
allStrings = false
break
}
}
if allStrings {
var b strings.Builder
b.Grow(totalLen)
b.WriteString(args[0].(string))
for i := 1; i < len(args); i++ {
if sep != empty {
b.WriteString(sep)
}
b.WriteString(args[i].(string))
}
return b.String()
}
// Fallback for mixed types: use pooled Builder.
b := New(sep)
b.Grow(estimateWith(sep, args))
b.Add(args...)
return b.Output()
}
// Wrap encloses concatenated args between before and after strings (no inner separator).
// Equivalent to Concat(before, args..., after).
func Wrap(before, after string, args ...any) string {
b := Start()
b.Grow(len(before) + len(after) + estimate(args))
b.Add(before)
b.Add(args...)
b.Add(after)
return b.Output()
}
// WrapEach wraps each argument individually with before/after, concatenated without separators.
// Applies before + arg + after to each arg.
// Preestimates size; uses pooled Builder if enabled.
// Returns empty if no args.
// Useful for wrapping multiple items identically without joins.
func WrapEach(before, after string, args ...any) string {
if len(args) == 0 {
return empty
}
total := (len(before)+len(after))*len(args) + estimate(args)
b := Start() // Use pooled builder, but we will write manually.
b.Grow(total)
for _, a := range args {
write(&b.buf, before)
write(&b.buf, a)
write(&b.buf, after)
}
// No separators were ever added, so this is safe.
b.needsSep = true // Correctly set state in case of reuse.
return b.Output()
}
// WrapWith encloses concatenated args between before and after strings,
// joining the arguments with the provided separator.
// If no args, returns before + after.
// Builds inner with With(sep, args...), then Concat(before, inner, after).
// Benefits from pooling via With and Concat.
func WrapWith(sep, before, after string, args ...any) string {
if len(args) == 0 {
return before + after
}
// First, efficiently build the inner part.
inner := With(sep, args...)
// Then, wrap it without allocating another slice.
b := Start()
b.Grow(len(before) + len(inner) + len(after))
b.Add(before)
b.Add(inner)
b.Add(after)
return b.Output()
}
// Pad surrounds a string with spaces on both sides.
// Ensures proper spacing for SQL operators like "=", "AND", etc.
// Example: Pad("=") returns " = " for cleaner formatting.
func Pad(s string) string {
return Concat(space, s, space)
}
// PadWith adds a separator before the string and a space after it.
// Useful for formatting SQL parts with custom leading separators.
// Example: PadWith(",", "column") returns ",column ".
func PadWith(sep, s string) string {
return Concat(sep, s, space)
}
// Parens wraps content in parentheses
// Useful for grouping SQL conditions or expressions
// Example: Parens("a = b AND c = d") → "(a = b AND c = d)"
func Parens(content string) string {
return Concat(parenOpen, content, parenClose)
}
// ParensWith wraps multiple arguments in parentheses with a separator
// Example: ParensWith(" AND ", "a = b", "c = d") → "(a = b AND c = d)"
func ParensWith(sep string, args ...any) string {
return Concat(parenOpen, With(sep, args...), parenClose)
}

376
vendor/github.com/olekukonko/cat/fn.go generated vendored Normal file
View file

@ -0,0 +1,376 @@
package cat
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"unsafe"
)
// write writes a value to the given strings.Builder using fast paths to avoid temporary allocations.
// It handles common types like strings, byte slices, integers, floats, and booleans directly for efficiency.
// For other types, it falls back to fmt.Fprint, which may involve allocations.
// This function is optimized for performance in string concatenation scenarios, prioritizing
// common cases like strings and numbers at the top of the type switch for compiler optimization.
// Note: For integers and floats, it uses stack-allocated buffers and strconv.Append* functions to
// convert numbers to strings without heap allocations.
func write(b *strings.Builder, arg any) {
writeValue(b, arg, 0)
}
// writeValue appends the string representation of arg to b, handling recursion with a depth limit.
// It serves as a recursive helper for write, directly handling primitives and delegating complex
// types to writeReflect. The depth parameter prevents excessive recursion in deeply nested structures.
func writeValue(b *strings.Builder, arg any, depth int) {
// Handle recursion depth limit
if depth > maxRecursionDepth {
b.WriteString("...")
return
}
// Handle nil values
if arg == nil {
b.WriteString(nilString)
return
}
// Fast path type switch for all primitive types
switch v := arg.(type) {
case string:
b.WriteString(v)
case []byte:
b.WriteString(bytesToString(v))
case int:
var buf [20]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int64:
var buf [20]byte
b.Write(strconv.AppendInt(buf[:0], v, 10))
case int32:
var buf [11]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int16:
var buf [6]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int8:
var buf [4]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case uint:
var buf [20]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint64:
var buf [20]byte
b.Write(strconv.AppendUint(buf[:0], v, 10))
case uint32:
var buf [10]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint16:
var buf [5]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint8:
var buf [3]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case float64:
var buf [24]byte
b.Write(strconv.AppendFloat(buf[:0], v, 'f', -1, 64))
case float32:
var buf [24]byte
b.Write(strconv.AppendFloat(buf[:0], float64(v), 'f', -1, 32))
case bool:
if v {
b.WriteString("true")
} else {
b.WriteString("false")
}
case fmt.Stringer:
b.WriteString(v.String())
case error:
b.WriteString(v.Error())
default:
// Fallback to reflection-based handling
writeReflect(b, arg, depth)
}
}
// writeReflect handles all complex types safely.
func writeReflect(b *strings.Builder, arg any, depth int) {
defer func() {
if r := recover(); r != nil {
b.WriteString("[!reflect panic!]")
}
}()
val := reflect.ValueOf(arg)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
b.WriteString(nilString)
return
}
val = val.Elem()
}
switch val.Kind() {
case reflect.Slice, reflect.Array:
b.WriteByte('[')
for i := 0; i < val.Len(); i++ {
if i > 0 {
b.WriteString(", ") // Use comma-space for readability
}
writeValue(b, val.Index(i).Interface(), depth+1)
}
b.WriteByte(']')
case reflect.Struct:
typ := val.Type()
b.WriteByte('{') // Use {} for structs to follow Go convention
first := true
for i := 0; i < val.NumField(); i++ {
fieldValue := val.Field(i)
if !fieldValue.CanInterface() {
continue // Skip unexported fields
}
if !first {
b.WriteByte(' ') // Use space as separator
}
first = false
b.WriteString(typ.Field(i).Name)
b.WriteByte(':')
writeValue(b, fieldValue.Interface(), depth+1)
}
b.WriteByte('}')
case reflect.Map:
b.WriteByte('{')
keys := val.MapKeys()
sort.Slice(keys, func(i, j int) bool {
// A simple string-based sort for keys
return fmt.Sprint(keys[i].Interface()) < fmt.Sprint(keys[j].Interface())
})
for i, key := range keys {
if i > 0 {
b.WriteByte(' ') // Use space as separator
}
writeValue(b, key.Interface(), depth+1)
b.WriteByte(':')
writeValue(b, val.MapIndex(key).Interface(), depth+1)
}
b.WriteByte('}')
case reflect.Interface:
if val.IsNil() {
b.WriteString(nilString)
return
}
writeValue(b, val.Elem().Interface(), depth+1)
default:
fmt.Fprint(b, arg)
}
}
// valueToString converts any value to a string representation.
// It uses optimized paths for common types to avoid unnecessary allocations.
// For types like integers and floats, it directly uses strconv functions.
// This function is useful for single-argument conversions or as a helper in other parts of the package.
// Unlike write, it returns a string instead of appending to a builder.
func valueToString(arg any) string {
switch v := arg.(type) {
case string:
return v
case []byte:
return bytesToString(v)
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case int32:
return strconv.FormatInt(int64(v), 10)
case uint:
return strconv.FormatUint(uint64(v), 10)
case uint64:
return strconv.FormatUint(v, 10)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case bool:
if v {
return "true"
}
return "false"
case fmt.Stringer:
return v.String()
case error:
return v.Error()
default:
return fmt.Sprint(v)
}
}
// estimateWith calculates a conservative estimate of the total string length when concatenating
// the given arguments with a separator. This is used for preallocating capacity in strings.Builder
// to minimize reallocations during building.
// It accounts for the length of separators and estimates the length of each argument based on its type.
// If no arguments are provided, it returns 0.
func estimateWith(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := len(sep) * (len(args) - 1)
size += estimate(args)
return size
}
// estimate calculates a conservative estimate of the combined string length of the given arguments.
// It iterates over each argument and adds an estimated length based on its type:
// - Strings and byte slices: exact length.
// - Numbers: calculated digit count using numLen or uNumLen.
// - Floats and others: fixed conservative estimates (e.g., 16 or 24 bytes).
// This helper is used internally by estimateWith and focuses solely on the arguments without separators.
func estimate(args []any) int {
var size int
for _, a := range args {
switch v := a.(type) {
case string:
size += len(v)
case []byte:
size += len(v)
case int:
size += numLen(int64(v))
case int8:
size += numLen(int64(v))
case int16:
size += numLen(int64(v))
case int32:
size += numLen(int64(v))
case int64:
size += numLen(v)
case uint:
size += uNumLen(uint64(v))
case uint8:
size += uNumLen(uint64(v))
case uint16:
size += uNumLen(uint64(v))
case uint32:
size += uNumLen(uint64(v))
case uint64:
size += uNumLen(v)
case float32:
size += 16
case float64:
size += 24
case bool:
size += 5 // "false"
case fmt.Stringer, error:
size += 16 // conservative
default:
size += 16 // conservative
}
}
return size
}
// numLen returns the number of characters required to represent the signed integer n as a string.
// It handles negative numbers by adding 1 for the '-' sign and uses a loop to count digits.
// Special handling for math.MinInt64 to avoid overflow when negating.
// Returns 1 for 0, and up to 20 for the largest values.
func numLen(n int64) int {
if n == 0 {
return 1
}
c := 0
if n < 0 {
c = 1 // for '-'
// NOTE: math.MinInt64 negated overflows; handle by adding one digit and returning 20.
if n == -1<<63 {
return 20
}
n = -n
}
for n > 0 {
n /= 10
c++
}
return c
}
// uNumLen returns the number of characters required to represent the unsigned integer n as a string.
// It uses a loop to count digits.
// Returns 1 for 0, and up to 20 for the largest uint64 values.
func uNumLen(n uint64) int {
if n == 0 {
return 1
}
c := 0
for n > 0 {
n /= 10
c++
}
return c
}
// bytesToString converts a byte slice to a string efficiently.
// If the package's UnsafeBytes flag is set (via IsUnsafeBytes()), it uses unsafe operations
// to create a string backed by the same memory as the byte slice, avoiding a copy.
// This is zero-allocation when unsafe is enabled.
// Falls back to standard string(bts) conversion otherwise.
// For empty slices, it returns a constant empty string.
// Compatible with Go 1.20+ unsafe functions like unsafe.String and unsafe.SliceData.
func bytesToString(bts []byte) string {
if len(bts) == 0 {
return empty
}
if IsUnsafeBytes() {
// Go 1.20+: unsafe.String with SliceData (1.20 introduced, 1.22 added SliceData).
return unsafe.String(unsafe.SliceData(bts), len(bts))
}
return string(bts)
}
// recursiveEstimate calculates the estimated string length for potentially nested arguments,
// including the lengths of separators between elements. It recurses on nested []any slices,
// flattening the structure while accounting for separators only between non-empty subparts.
// This function is useful for preallocating capacity in builders for nested concatenation operations.
func recursiveEstimate(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := 0
needsSep := false
for _, a := range args {
switch v := a.(type) {
case []any:
subSize := recursiveEstimate(sep, v)
if subSize > 0 {
if needsSep {
size += len(sep)
}
size += subSize
needsSep = true
}
default:
if needsSep {
size += len(sep)
}
size += estimate([]any{a})
needsSep = true
}
}
return size
}
// recursiveAdd appends the string representations of potentially nested arguments to the builder.
// It recurses on nested []any slices, effectively flattening the structure by adding leaf values
// directly via b.Add without inserting separators (separators are handled externally if needed).
// This function is designed for efficient concatenation of nested argument lists.
func recursiveAdd(b *Builder, args []any) {
for _, a := range args {
switch v := a.(type) {
case []any:
recursiveAdd(b, v)
default:
b.Add(a)
}
}
}

161
vendor/github.com/olekukonko/cat/sql.go generated vendored Normal file
View file

@ -0,0 +1,161 @@
package cat
// On builds a SQL ON clause comparing two columns across tables.
// Formats as: "table1.column1 = table2.column2" with proper spacing.
// Useful in JOIN conditions to match keys between tables.
func On(table1, column1, table2, column2 string) string {
return With(space,
With(dot, table1, column1),
Pad(equal),
With(dot, table2, column2),
)
}
// Using builds a SQL condition comparing two aliased columns.
// Formats as: "alias1.column1 = alias2.column2" for JOINs or filters.
// Helps when working with table aliases in complex queries.
func Using(alias1, column1, alias2, column2 string) string {
return With(space,
With(dot, alias1, column1),
Pad(equal),
With(dot, alias2, column2),
)
}
// And joins multiple SQL conditions with the AND operator.
// Adds spacing to ensure clean SQL output (e.g., "cond1 AND cond2").
// Accepts variadic arguments for flexible condition chaining.
func And(conditions ...any) string {
return With(Pad(and), conditions...)
}
// In creates a SQL IN clause with properly quoted values
// Example: In("status", "active", "pending") → "status IN ('active', 'pending')"
// Handles value quoting and comma separation automatically
func In(column string, values ...string) string {
if len(values) == 0 {
return Concat(column, inOpen, inClose)
}
quotedValues := make([]string, len(values))
for i, v := range values {
quotedValues[i] = "'" + v + "'"
}
return Concat(column, inOpen, JoinWith(comma+space, quotedValues...), inClose)
}
// As creates an aliased SQL expression
// Example: As("COUNT(*)", "total_count") → "COUNT(*) AS total_count"
func As(expression, alias string) string {
return Concat(expression, asSQL, alias)
}
// Count creates a COUNT expression with optional alias
// Example: Count("id") → "COUNT(id)"
// Example: Count("id", "total") → "COUNT(id) AS total"
// Example: Count("DISTINCT user_id", "unique_users") → "COUNT(DISTINCT user_id) AS unique_users"
func Count(column string, alias ...string) string {
expression := Concat(count, column, parenClose)
if len(alias) == 0 {
return expression
}
return As(expression, alias[0])
}
// CountAll creates COUNT(*) with optional alias
// Example: CountAll() → "COUNT(*)"
// Example: CountAll("total") → "COUNT(*) AS total"
func CountAll(alias ...string) string {
if len(alias) == 0 {
return countAll
}
return As(countAll, alias[0])
}
// Sum creates a SUM expression with optional alias
// Example: Sum("amount") → "SUM(amount)"
// Example: Sum("amount", "total") → "SUM(amount) AS total"
func Sum(column string, alias ...string) string {
expression := Concat(sum, column, parenClose)
if len(alias) == 0 {
return expression
}
return As(expression, alias[0])
}
// Avg creates an AVG expression with optional alias
// Example: Avg("score") → "AVG(score)"
// Example: Avg("score", "average") → "AVG(score) AS average"
func Avg(column string, alias ...string) string {
expression := Concat(avg, column, parenClose)
if len(alias) == 0 {
return expression
}
return As(expression, alias[0])
}
// Max creates a MAX expression with optional alias
// Example: Max("price") → "MAX(price)"
// Example: Max("price", "max_price") → "MAX(price) AS max_price"
func Max(column string, alias ...string) string {
expression := Concat(maxOpen, column, parenClose)
if len(alias) == 0 {
return expression
}
return As(expression, alias[0])
}
// Min creates a MIN expression with optional alias
// Example: Min("price") → "MIN(price)"
// Example: Min("price", "min_price") → "MIN(price) AS min_price"
func Min(column string, alias ...string) string {
expression := Concat(minOpen, column, parenClose)
if len(alias) == 0 {
return expression
}
return As(expression, alias[0])
}
// Case creates a SQL CASE expression with optional alias
// Example: Case("WHEN status = 'active' THEN 1 ELSE 0 END", "is_active") → "CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active"
func Case(expression string, alias ...string) string {
caseExpr := Concat(caseSQL, expression)
if len(alias) == 0 {
return caseExpr
}
return As(caseExpr, alias[0])
}
// CaseWhen creates a complete SQL CASE expression from individual parts with proper value handling
// Example: CaseWhen("status =", "'active'", "1", "0", "is_active") → "CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active"
// Example: CaseWhen("age >", "18", "'adult'", "'minor'", "age_group") → "CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END AS age_group"
func CaseWhen(conditionPart string, conditionValue, thenValue, elseValue any, alias ...string) string {
condition := Concat(conditionPart, valueToString(conditionValue))
expression := Concat(
when, condition, then, valueToString(thenValue), elseSQL, valueToString(elseValue), end,
)
return Case(expression, alias...)
}
// CaseWhenMulti creates a SQL CASE expression with multiple WHEN clauses
// Example: CaseWhenMulti([]string{"status =", "age >"}, []any{"'active'", 18}, []any{1, "'adult'"}, 0, "result") → "CASE WHEN status = 'active' THEN 1 WHEN age > 18 THEN 'adult' ELSE 0 END AS result"
func CaseWhenMulti(conditionParts []string, conditionValues, thenValues []any, elseValue any, alias ...string) string {
if len(conditionParts) != len(conditionValues) || len(conditionParts) != len(thenValues) {
return "" // or handle error
}
var whenClauses []string
for i := 0; i < len(conditionParts); i++ {
condition := Concat(conditionParts[i], valueToString(conditionValues[i]))
whenClause := Concat(when, condition, then, valueToString(thenValues[i]))
whenClauses = append(whenClauses, whenClause)
}
expression := Concat(
JoinWith(space, whenClauses...),
elseSQL,
valueToString(elseValue),
end,
)
return Case(expression, alias...)
}

View file

@ -222,6 +222,13 @@ logger.Info("Slog log") // Output: level=INFO msg="Slog log" namespace=app class
ll.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] (see example/stack.png) ll.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] (see example/stack.png)
``` ```
4**General Output**
Logs a output in structured way for inspection of public & private values.
```go
ll.Handler(lh.NewColorizedHandler(os.Stdout))
ll.Output(&SomeStructWithPrivateValues{})
```
#### Performance Tracking #### Performance Tracking
Measure execution time for performance analysis. Measure execution time for performance analysis.
```go ```go

View file

@ -1,421 +0,0 @@
package ll
import (
"fmt"
"github.com/olekukonko/ll/lx"
"reflect"
"strconv"
"strings"
"unsafe"
)
const (
maxRecursionDepth = 20 // Maximum depth for recursive type handling to prevent stack overflow
nilString = "<nil>" // String representation for nil values
unexportedString = "<?>" // String representation for unexported fields
)
// Concat efficiently concatenates values without a separator using the default logger.
// It converts each argument to a string and joins them directly, optimizing for performance
// in logging scenarios. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.Concat("Hello", 42, true) // Returns "Hello42true"
func Concat(args ...any) string {
return concat(args...)
}
// ConcatSpaced concatenates values with a space separator using the default logger.
// It converts each argument to a string and joins them with spaces, suitable for log message
// formatting. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.ConcatSpaced("Hello", 42, true) // Returns "Hello 42 true"
func ConcatSpaced(args ...any) string {
return concatSpaced(args...)
}
// ConcatAll concatenates elements with a separator, prefix, and suffix using the default logger.
// It combines before, main, and after arguments with the specified separator, optimizing memory
// allocation for logging. Thread-safe as it does not modify shared state.
// Example:
//
// msg := ll.ConcatAll(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns "prefix,main,suffix"
func ConcatAll(sep string, before, after []any, args ...any) string {
return concatenate(sep, before, after, args...)
}
// concat efficiently concatenates values without a separator.
// It converts each argument to a string and joins them directly, optimizing for performance
// in logging scenarios. Used internally by Concat and other logging functions.
// Example:
//
// msg := concat("Hello", 42, true) // Returns "Hello42true"
func concat(args ...any) string {
return concatWith("", args...)
}
// concatSpaced concatenates values with a space separator.
// It converts each argument to a string and joins them with spaces, suitable for formatting
// log messages. Used internally by ConcatSpaced.
// Example:
//
// msg := concatSpaced("Hello", 42, true) // Returns "Hello 42 true"
func concatSpaced(args ...any) string {
return concatWith(lx.Space, args...)
}
// concatWith concatenates values with a specified separator using optimized type handling.
// It builds a string from arguments, handling various types efficiently (strings, numbers,
// structs, etc.), and is used by concat and concatSpaced for log message construction.
// Thread-safe as it does not modify shared state.
// Example:
//
// msg := concatWith(",", "Hello", 42, true) // Returns "Hello,42,true"
func concatWith(sep string, args ...any) string {
switch len(args) {
case 0:
return ""
case 1:
return concatToString(args[0])
}
var b strings.Builder
b.Grow(concatEstimateArgs(sep, args))
for i, arg := range args {
if i > 0 {
b.WriteString(sep)
}
concatWriteValue(&b, arg, 0)
}
return b.String()
}
// concatenate concatenates elements with separators, prefixes, and suffixes efficiently.
// It combines before, main, and after arguments with the specified separator, optimizing
// memory allocation for complex log message formatting. Used internally by ConcatAll.
// Example:
//
// msg := concatenate(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns "prefix,main,suffix"
func concatenate(sep string, before []any, after []any, args ...any) string {
totalLen := len(before) + len(after) + len(args)
switch totalLen {
case 0:
return ""
case 1:
switch {
case len(before) > 0:
return concatToString(before[0])
case len(args) > 0:
return concatToString(args[0])
default:
return concatToString(after[0])
}
}
var b strings.Builder
b.Grow(concatEstimateTotal(sep, before, after, args))
// Write before elements
concatWriteGroup(&b, sep, before)
// Write main arguments
if len(before) > 0 && len(args) > 0 {
b.WriteString(sep)
}
concatWriteGroup(&b, sep, args)
// Write after elements
if len(after) > 0 && (len(before) > 0 || len(args) > 0) {
b.WriteString(sep)
}
concatWriteGroup(&b, sep, after)
return b.String()
}
// concatWriteGroup writes a group of arguments to a strings.Builder with a separator.
// It handles each argument by converting it to a string, used internally by concatenate
// to process before, main, or after groups in log message construction.
// Example:
//
// var b strings.Builder
// concatWriteGroup(&b, ",", []any{"a", 42}) // Writes "a,42" to b
func concatWriteGroup(b *strings.Builder, sep string, group []any) {
for i, arg := range group {
if i > 0 {
b.WriteString(sep)
}
concatWriteValue(b, arg, 0)
}
}
// concatToString converts a single argument to a string efficiently.
// It handles common types (string, []byte, fmt.Stringer) with minimal overhead and falls
// back to fmt.Sprint for other types. Used internally by concat and concatenate.
// Example:
//
// s := concatToString("Hello") // Returns "Hello"
// s := concatToString([]byte{65, 66}) // Returns "AB"
func concatToString(arg any) string {
switch v := arg.(type) {
case string:
return v
case []byte:
return *(*string)(unsafe.Pointer(&v))
case fmt.Stringer:
return v.String()
case error:
return v.Error()
default:
return fmt.Sprint(v)
}
}
// concatEstimateTotal estimates the total string length for concatenate.
// It calculates the expected size of the concatenated string, including before, main, and
// after arguments with separators, to preallocate the strings.Builder capacity.
// Example:
//
// size := concatEstimateTotal(",", []any{"prefix"}, []any{"suffix"}, "main")
// // Returns estimated length for "prefix,main,suffix"
func concatEstimateTotal(sep string, before, after, args []any) int {
size := 0
if len(before) > 0 {
size += concatEstimateArgs(sep, before)
}
if len(args) > 0 {
if size > 0 {
size += len(sep)
}
size += concatEstimateArgs(sep, args)
}
if len(after) > 0 {
if size > 0 {
size += len(sep)
}
size += concatEstimateArgs(sep, after)
}
return size
}
// concatEstimateArgs estimates the string length for a group of arguments.
// It sums the estimated sizes of each argument plus separators, used by concatEstimateTotal
// and concatWith to optimize memory allocation for log message construction.
// Example:
//
// size := concatEstimateArgs(",", []any{"hello", 42}) // Returns estimated length for "hello,42"
func concatEstimateArgs(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := len(sep) * (len(args) - 1)
for _, arg := range args {
size += concatEstimateSize(arg)
}
return size
}
// concatEstimateSize estimates the string length for a single argument.
// It provides size estimates for various types (strings, numbers, booleans, etc.) to
// optimize strings.Builder capacity allocation in logging functions.
// Example:
//
// size := concatEstimateSize("hello") // Returns 5
// size := concatEstimateSize(42) // Returns ~2
func concatEstimateSize(arg any) int {
switch v := arg.(type) {
case string:
return len(v)
case []byte:
return len(v)
case int:
return concatNumLen(int64(v))
case int64:
return concatNumLen(v)
case int32:
return concatNumLen(int64(v))
case int16:
return concatNumLen(int64(v))
case int8:
return concatNumLen(int64(v))
case uint:
return concatNumLen(uint64(v))
case uint64:
return concatNumLen(v)
case uint32:
return concatNumLen(uint64(v))
case uint16:
return concatNumLen(uint64(v))
case uint8:
return concatNumLen(uint64(v))
case float64:
return 24 // Max digits for float64
case float32:
return 16 // Max digits for float32
case bool:
if v {
return 4 // "true"
}
return 5 // "false"
case fmt.Stringer:
return 16 // Conservative estimate
default:
return 16 // Default estimate
}
}
// concatNumLen estimates the string length for a signed or unsigned integer.
// It returns a conservative estimate (20 digits) for int64 or uint64 values, including
// a sign for negative numbers, used by concatEstimateSize for memory allocation.
// Example:
//
// size := concatNumLen(int64(-123)) // Returns 20
// size := concatNumLen(uint64(123)) // Returns 20
func concatNumLen[T int64 | uint64](v T) int {
if v < 0 {
return 20 // Max digits for int64 + sign
}
return 20 // Max digits for uint64
}
// concatWriteValue writes a formatted value to a strings.Builder with recursion depth tracking.
// It handles various types (strings, numbers, structs, slices, etc.) and prevents infinite
// recursion by limiting depth. Used internally by concatWith and concatWriteGroup for log
// message formatting.
// Example:
//
// var b strings.Builder
// concatWriteValue(&b, "hello", 0) // Writes "hello" to b
// concatWriteValue(&b, []int{1, 2}, 0) // Writes "[1,2]" to b
func concatWriteValue(b *strings.Builder, arg any, depth int) {
if depth > maxRecursionDepth {
b.WriteString("...")
return
}
if arg == nil {
b.WriteString(nilString)
return
}
if s, ok := arg.(fmt.Stringer); ok {
b.WriteString(s.String())
return
}
switch v := arg.(type) {
case string:
b.WriteString(v)
case []byte:
b.Write(v)
case int:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int64:
b.WriteString(strconv.FormatInt(v, 10))
case int32:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int16:
b.WriteString(strconv.FormatInt(int64(v), 10))
case int8:
b.WriteString(strconv.FormatInt(int64(v), 10))
case uint:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint64:
b.WriteString(strconv.FormatUint(v, 10))
case uint32:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint16:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case uint8:
b.WriteString(strconv.FormatUint(uint64(v), 10))
case float64:
b.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
case float32:
b.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
case bool:
if v {
b.WriteString("true")
} else {
b.WriteString("false")
}
default:
val := reflect.ValueOf(arg)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
b.WriteString(nilString)
return
}
val = val.Elem()
}
switch val.Kind() {
case reflect.Slice, reflect.Array:
concatFormatSlice(b, val, depth)
case reflect.Struct:
concatFormatStruct(b, val, depth)
default:
fmt.Fprint(b, v)
}
}
}
// concatFormatSlice formats a slice or array for logging.
// It writes the elements in a bracketed, comma-separated format, handling nested types
// recursively with depth tracking. Used internally by concatWriteValue for log message formatting.
// Example:
//
// var b strings.Builder
// val := reflect.ValueOf([]int{1, 2})
// concatFormatSlice(&b, val, 0) // Writes "[1,2]" to b
func concatFormatSlice(b *strings.Builder, val reflect.Value, depth int) {
b.WriteByte('[')
for i := 0; i < val.Len(); i++ {
if i > 0 {
b.WriteByte(',')
}
concatWriteValue(b, val.Index(i).Interface(), depth+1)
}
b.WriteByte(']')
}
// concatFormatStruct formats a struct for logging.
// It writes the structs exported fields in a bracketed, name:value format, handling nested
// types recursively with depth tracking. Unexported fields are represented as "<?>".
// Used internally by concatWriteValue for log message formatting.
// Example:
//
// var b strings.Builder
// val := reflect.ValueOf(struct{ Name string }{Name: "test"})
// concatFormatStruct(&b, val, 0) // Writes "[Name:test]" to b
func concatFormatStruct(b *strings.Builder, val reflect.Value, depth int) {
typ := val.Type()
b.WriteByte('[')
first := true
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldValue := val.Field(i)
if !first {
b.WriteString("; ")
}
first = false
b.WriteString(field.Name)
b.WriteByte(':')
if !fieldValue.CanInterface() {
b.WriteString(unexportedString)
continue
}
concatWriteValue(b, fieldValue.Interface(), depth+1)
}
b.WriteByte(']')
}

View file

@ -2,6 +2,7 @@ package ll
import ( import (
"fmt" "fmt"
"github.com/olekukonko/cat"
"github.com/olekukonko/ll/lx" "github.com/olekukonko/ll/lx"
"os" "os"
"strings" "strings"
@ -50,7 +51,7 @@ func (fb *FieldBuilder) Info(args ...any) {
return return
} }
// Log at Info level with the builders fields, no stack trace // Log at Info level with the builders fields, no stack trace
fb.logger.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), fb.fields, false) fb.logger.log(lx.LevelInfo, lx.ClassText, cat.Space(args...), fb.fields, false)
} }
// Infof logs a message at Info level with the builders fields. // Infof logs a message at Info level with the builders fields.
@ -85,7 +86,7 @@ func (fb *FieldBuilder) Debug(args ...any) {
return return
} }
// Log at Debug level with the builders fields, no stack trace // Log at Debug level with the builders fields, no stack trace
fb.logger.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), fb.fields, false) fb.logger.log(lx.LevelDebug, lx.ClassText, cat.Space(args...), fb.fields, false)
} }
// Debugf logs a message at Debug level with the builders fields. // Debugf logs a message at Debug level with the builders fields.
@ -120,7 +121,7 @@ func (fb *FieldBuilder) Warn(args ...any) {
return return
} }
// Log at Warn level with the builders fields, no stack trace // Log at Warn level with the builders fields, no stack trace
fb.logger.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), fb.fields, false) fb.logger.log(lx.LevelWarn, lx.ClassText, cat.Space(args...), fb.fields, false)
} }
// Warnf logs a message at Warn level with the builders fields. // Warnf logs a message at Warn level with the builders fields.
@ -154,7 +155,7 @@ func (fb *FieldBuilder) Error(args ...any) {
return return
} }
// Log at Error level with the builders fields, no stack trace // Log at Error level with the builders fields, no stack trace
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, false) fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, false)
} }
// Errorf logs a message at Error level with the builders fields. // Errorf logs a message at Error level with the builders fields.
@ -188,7 +189,7 @@ func (fb *FieldBuilder) Stack(args ...any) {
return return
} }
// Log at Error level with the builders fields and a stack trace // Log at Error level with the builders fields and a stack trace
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, true) fb.logger.log(lx.LevelError, lx.ClassText, cat.Space(args...), fb.fields, true)
} }
// Stackf logs a message at Error level with a stack trace and the builders fields. // Stackf logs a message at Error level with a stack trace and the builders fields.

View file

@ -1,11 +1,12 @@
package ll package ll
import ( import (
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
"os" "os"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
) )
// defaultLogger is the global logger instance for package-level logging functions. // defaultLogger is the global logger instance for package-level logging functions.
@ -468,13 +469,7 @@ func Len() int64 {
// duration := ll.Measure(func() { time.Sleep(time.Millisecond) }) // duration := ll.Measure(func() { time.Sleep(time.Millisecond) })
// // Output: [] INFO: function executed [duration=~1ms] // // Output: [] INFO: function executed [duration=~1ms]
func Measure(fns ...func()) time.Duration { func Measure(fns ...func()) time.Duration {
start := time.Now() return defaultLogger.Measure(fns...)
for _, fn := range fns {
fn()
}
duration := time.Since(start)
defaultLogger.Fields("duration", duration).Infof("function executed")
return duration
} }
// Benchmark logs the duration since a start time at Info level using the default logger. // Benchmark logs the duration since a start time at Info level using the default logger.
@ -486,7 +481,7 @@ func Measure(fns ...func()) time.Duration {
// time.Sleep(time.Millisecond) // time.Sleep(time.Millisecond)
// ll.Benchmark(start) // Output: [] INFO: benchmark [start=... end=... duration=...] // ll.Benchmark(start) // Output: [] INFO: benchmark [start=... end=... duration=...]
func Benchmark(start time.Time) { func Benchmark(start time.Time) {
defaultLogger.Fields("start", start, "end", time.Now(), "duration", time.Now().Sub(start)).Infof("benchmark") defaultLogger.Benchmark(start)
} }
// Clone returns a new logger with the same configuration as the default logger. // Clone returns a new logger with the same configuration as the default logger.
@ -657,3 +652,11 @@ func Mark(names ...string) {
defaultLogger.mark(2, names...) defaultLogger.mark(2, names...)
} }
// Output logs data in a human-readable JSON format at Info level, including caller file and line information.
// It is similar to Dbg but formats the output as JSON for better readability. It is thread-safe and respects
// the loggers configuration (e.g., enabled, level, suspend, handler, middleware).
func Output(values ...interface{}) {
o := NewInspector(defaultLogger)
o.Log(2, values...)
}

239
vendor/github.com/olekukonko/ll/inspector.go generated vendored Normal file
View file

@ -0,0 +1,239 @@
package ll
import (
"encoding/json"
"fmt"
"reflect"
"runtime"
"strings"
"unsafe"
"github.com/olekukonko/ll/lx"
)
// Inspector is a utility for Logger that provides advanced inspection and logging of data
// in human-readable JSON format. It uses reflection to access and represent unexported fields,
// nested structs, embedded structs, and pointers, making it useful for debugging complex data structures.
type Inspector struct {
logger *Logger
}
// NewInspector returns a new Inspector instance associated with the provided logger.
func NewInspector(logger *Logger) *Inspector {
return &Inspector{logger: logger}
}
// Log outputs the given values as indented JSON at the Info level, prefixed with the caller's
// file name and line number. It handles structs (including unexported fields, nested, and embedded),
// pointers, errors, and other types. The skip parameter determines how many stack frames to skip
// when identifying the caller; typically set to 2 to account for the call to Log and its wrapper.
//
// Example usage within a Logger method:
//
// o := NewInspector(l)
// o.Log(2, someStruct) // Logs JSON representation with caller info
func (o *Inspector) Log(skip int, values ...interface{}) {
// Skip if logger is suspended or Info level is disabled
if o.logger.suspend.Load() || !o.logger.shouldLog(lx.LevelInfo) {
return
}
// Retrieve caller information for logging context
_, file, line, ok := runtime.Caller(skip)
if !ok {
o.logger.log(lx.LevelError, lx.ClassText, "Inspector: Unable to parse runtime caller", nil, false)
return
}
// Extract short filename for concise output
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
// Process each value individually
for _, value := range values {
var jsonData []byte
var err error
// Use reflection for struct types to handle unexported and nested fields
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() == reflect.Struct {
valueMap := o.structToMap(val)
jsonData, err = json.MarshalIndent(valueMap, "", " ")
} else if errVal, ok := value.(error); ok {
// Special handling for errors to represent them as a simple map
value = map[string]string{"error": errVal.Error()}
jsonData, err = json.MarshalIndent(value, "", " ")
} else {
// Fall back to standard JSON marshaling for non-struct types
jsonData, err = json.MarshalIndent(value, "", " ")
}
if err != nil {
o.logger.log(lx.LevelError, lx.ClassText, fmt.Sprintf("Inspector: JSON encoding error: %v", err), nil, false)
continue
}
// Construct log message with file, line, and JSON data
msg := fmt.Sprintf("[%s:%d] DUMP: %s", shortFile, line, string(jsonData))
o.logger.log(lx.LevelInfo, lx.ClassText, msg, nil, false)
}
}
// structToMap recursively converts a struct's reflect.Value to a map[string]interface{}.
// It includes unexported fields (named with parentheses), prefixes pointers with '*',
// flattens anonymous embedded structs without json tags, and uses unsafe pointers to access
// unexported primitive fields when reflect.CanInterface() returns false.
func (o *Inspector) structToMap(val reflect.Value) map[string]interface{} {
result := make(map[string]interface{})
if !val.IsValid() {
return result
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Determine field name: prefer json tag if present and not "-", else use struct field name
baseName := fieldType.Name
jsonTag := fieldType.Tag.Get("json")
hasJsonTag := false
if jsonTag != "" {
if idx := strings.Index(jsonTag, ","); idx != -1 {
jsonTag = jsonTag[:idx]
}
if jsonTag != "-" {
baseName = jsonTag
hasJsonTag = true
}
}
// Enclose unexported field names in parentheses
fieldName := baseName
if !fieldType.IsExported() {
fieldName = "(" + baseName + ")"
}
// Handle pointer fields
isPtr := fieldType.Type.Kind() == reflect.Ptr
if isPtr {
fieldName = "*" + fieldName
if field.IsNil() {
result[fieldName] = nil
continue
}
field = field.Elem()
}
// Recurse for struct fields
if field.Kind() == reflect.Struct {
subMap := o.structToMap(field)
isNested := !fieldType.Anonymous || hasJsonTag
if isNested {
result[fieldName] = subMap
} else {
// Flatten embedded struct fields into the parent map, avoiding overwrites
for k, v := range subMap {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
} else {
// Handle primitive fields
if field.CanInterface() {
result[fieldName] = field.Interface()
} else {
// Use unsafe access for unexported primitives
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.String:
result[fieldName] = *(*string)(ptr)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
result[fieldName] = o.getIntFromUnexportedField(field)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
result[fieldName] = o.getUintFromUnexportedField(field)
case reflect.Float32, reflect.Float64:
result[fieldName] = o.getFloatFromUnexportedField(field)
case reflect.Bool:
result[fieldName] = *(*bool)(ptr)
default:
result[fieldName] = fmt.Sprintf("<unexported %s>", field.Type().String())
}
}
}
}
return result
}
// emptyInterface represents the internal structure of an empty interface{}.
// This is used for unsafe pointer manipulation to access unexported field data.
type emptyInterface struct {
typ unsafe.Pointer
word unsafe.Pointer
}
// getDataPtr returns an unsafe.Pointer to the underlying data of a reflect.Value.
// This enables direct access to unexported fields via unsafe operations.
func getDataPtr(v reflect.Value) unsafe.Pointer {
return (*emptyInterface)(unsafe.Pointer(&v)).word
}
// getIntFromUnexportedField extracts a signed integer value from an unexported field
// using unsafe pointer access. It supports int, int8, int16, int32, and int64 kinds,
// returning the value as int64. Returns 0 for unsupported kinds.
func (o *Inspector) getIntFromUnexportedField(field reflect.Value) int64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Int:
return int64(*(*int)(ptr))
case reflect.Int8:
return int64(*(*int8)(ptr))
case reflect.Int16:
return int64(*(*int16)(ptr))
case reflect.Int32:
return int64(*(*int32)(ptr))
case reflect.Int64:
return *(*int64)(ptr)
}
return 0
}
// getUintFromUnexportedField extracts an unsigned integer value from an unexported field
// using unsafe pointer access. It supports uint, uint8, uint16, uint32, and uint64 kinds,
// returning the value as uint64. Returns 0 for unsupported kinds.
func (o *Inspector) getUintFromUnexportedField(field reflect.Value) uint64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Uint:
return uint64(*(*uint)(ptr))
case reflect.Uint8:
return uint64(*(*uint8)(ptr))
case reflect.Uint16:
return uint64(*(*uint16)(ptr))
case reflect.Uint32:
return uint64(*(*uint32)(ptr))
case reflect.Uint64:
return *(*uint64)(ptr)
}
return 0
}
// getFloatFromUnexportedField extracts a floating-point value from an unexported field
// using unsafe pointer access. It supports float32 and float64 kinds, returning the value
// as float64. Returns 0 for unsupported kinds.
func (o *Inspector) getFloatFromUnexportedField(field reflect.Value) float64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Float32:
return float64(*(*float32)(ptr))
case reflect.Float64:
return *(*float64)(ptr)
}
return 0
}

View file

@ -2,12 +2,14 @@ package lh
import ( import (
"fmt" "fmt"
"github.com/olekukonko/ll/lx"
"io" "io"
"os" "os"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/olekukonko/ll/lx"
) )
// Palette defines ANSI color codes for various log components. // Palette defines ANSI color codes for various log components.
@ -81,6 +83,7 @@ type ColorizedHandler struct {
palette Palette // Color scheme for formatting palette Palette // Color scheme for formatting
showTime bool // Whether to display timestamps showTime bool // Whether to display timestamps
timeFormat string // Format for timestamps (defaults to time.RFC3339) timeFormat string // Format for timestamps (defaults to time.RFC3339)
mu sync.Mutex
} }
// ColorOption defines a configuration function for ColorizedHandler. // ColorOption defines a configuration function for ColorizedHandler.
@ -130,6 +133,10 @@ func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler {
// //
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
func (h *ColorizedHandler) Handle(e *lx.Entry) error { func (h *ColorizedHandler) Handle(e *lx.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
switch e.Class { switch e.Class {
case lx.ClassDump: case lx.ClassDump:
// Handle hex dump entries // Handle hex dump entries

View file

@ -2,11 +2,13 @@ package lh
import ( import (
"fmt" "fmt"
"github.com/olekukonko/ll/lx"
"io" "io"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/olekukonko/ll/lx"
) )
// TextHandler is a handler that outputs log entries as plain text. // TextHandler is a handler that outputs log entries as plain text.
@ -17,6 +19,7 @@ type TextHandler struct {
w io.Writer // Destination for formatted log output w io.Writer // Destination for formatted log output
showTime bool // Whether to display timestamps showTime bool // Whether to display timestamps
timeFormat string // Format for timestamps (defaults to time.RFC3339) timeFormat string // Format for timestamps (defaults to time.RFC3339)
mu sync.Mutex
} }
// NewTextHandler creates a new TextHandler writing to the specified writer. // NewTextHandler creates a new TextHandler writing to the specified writer.
@ -55,6 +58,9 @@ func (h *TextHandler) Timestamped(enable bool, format ...string) {
// //
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test" // handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
func (h *TextHandler) Handle(e *lx.Entry) error { func (h *TextHandler) Handle(e *lx.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
// Special handling for dump output // Special handling for dump output
if e.Class == lx.ClassDump { if e.Class == lx.ClassDump {
return h.handleDumpOutput(e) return h.handleDumpOutput(e)

113
vendor/github.com/olekukonko/ll/ll.go generated vendored
View file

@ -5,8 +5,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
"io" "io"
"math" "math"
"os" "os"
@ -16,6 +14,10 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/olekukonko/cat"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/ll/lx"
) )
// Logger manages logging configuration and behavior, encapsulating state such as enablement, // Logger manages logging configuration and behavior, encapsulating state such as enablement,
@ -24,7 +26,7 @@ import (
type Logger struct { type Logger struct {
mu sync.RWMutex // Guards concurrent access to fields mu sync.RWMutex // Guards concurrent access to fields
enabled bool // Determines if logging is enabled enabled bool // Determines if logging is enabled
suspend bool // uses suspend path for most actions eg. skipping namespace checks suspend atomic.Bool // uses suspend path for most actions eg. skipping namespace checks
level lx.LevelType // Minimum log level (e.g., Debug, Info, Warn, Error) level lx.LevelType // Minimum log level (e.g., Debug, Info, Warn, Error)
namespaces *lx.Namespace // Manages namespace enable/disable states namespaces *lx.Namespace // Manages namespace enable/disable states
currentPath string // Current namespace path (e.g., "parent/child") currentPath string // Current namespace path (e.g., "parent/child")
@ -97,7 +99,11 @@ func (l *Logger) AddContext(key string, value interface{}) *Logger {
// logger.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...] // logger.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...]
func (l *Logger) Benchmark(start time.Time) time.Duration { func (l *Logger) Benchmark(start time.Time) time.Duration {
duration := time.Since(start) duration := time.Since(start)
l.Fields("start", start, "end", time.Now(), "duration", duration).Infof("benchmark") l.Fields(
"duration_ms", duration.Milliseconds(),
"duration", duration.String(),
).Infof("benchmark completed")
return duration return duration
} }
@ -220,7 +226,7 @@ func (l *Logger) Dbg(values ...interface{}) {
// logger.Debug("Debugging") // Output: [app] DEBUG: Debugging // logger.Debug("Debugging") // Output: [app] DEBUG: Debugging
func (l *Logger) Debug(args ...any) { func (l *Logger) Debug(args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -229,7 +235,7 @@ func (l *Logger) Debug(args ...any) {
return return
} }
l.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), nil, false) l.log(lx.LevelDebug, lx.ClassText, cat.Space(args...), nil, false)
} }
// Debugf logs a formatted message at Debug level, delegating to Debug. It is thread-safe. // Debugf logs a formatted message at Debug level, delegating to Debug. It is thread-safe.
@ -239,7 +245,7 @@ func (l *Logger) Debug(args ...any) {
// logger.Debugf("Debug %s", "message") // Output: [app] DEBUG: Debug message // logger.Debugf("Debug %s", "message") // Output: [app] DEBUG: Debug message
func (l *Logger) Debugf(format string, args ...any) { func (l *Logger) Debugf(format string, args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -344,6 +350,21 @@ func (l *Logger) Dump(values ...interface{}) {
} }
} }
// Output logs data in a human-readable JSON format at Info level, including caller file and line information.
// It is similar to Dbg but formats the output as JSON for better readability. It is thread-safe and respects
// the logger's configuration (e.g., enabled, level, suspend, handler, middleware).
// Example:
//
// logger := New("app").Enable()
// x := map[string]int{"key": 42}
// logger.Output(x) // Output: [app] INFO: [file.go:123] JSON: {"key": 42}
//
// Logger method to provide access to Output functionality
func (l *Logger) Output(values ...interface{}) {
o := NewInspector(l)
o.Log(2, values...)
}
// Enable activates logging, allowing logs to be emitted if other conditions (e.g., level, // Enable activates logging, allowing logs to be emitted if other conditions (e.g., level,
// namespace) are met. It is thread-safe using a write lock and returns the logger for chaining. // namespace) are met. It is thread-safe using a write lock and returns the logger for chaining.
// Example: // Example:
@ -432,7 +453,7 @@ func (l *Logger) Err(errs ...error) {
// logger.Error("Error occurred") // Output: [app] ERROR: Error occurred // logger.Error("Error occurred") // Output: [app] ERROR: Error occurred
func (l *Logger) Error(args ...any) { func (l *Logger) Error(args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -440,7 +461,7 @@ func (l *Logger) Error(args ...any) {
if !l.shouldLog(lx.LevelError) { if !l.shouldLog(lx.LevelError) {
return return
} }
l.log(lx.LevelError, lx.ClassText, concatSpaced(args...), nil, false) l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, false)
} }
// Errorf logs a formatted message at Error level, delegating to Error. It is thread-safe. // Errorf logs a formatted message at Error level, delegating to Error. It is thread-safe.
@ -450,7 +471,7 @@ func (l *Logger) Error(args ...any) {
// logger.Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred // logger.Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred
func (l *Logger) Errorf(format string, args ...any) { func (l *Logger) Errorf(format string, args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -465,7 +486,7 @@ func (l *Logger) Errorf(format string, args ...any) {
// logger.Fatal("Fatal error") // Output: [app] ERROR: Fatal error [stack=...], then exits // logger.Fatal("Fatal error") // Output: [app] ERROR: Fatal error [stack=...], then exits
func (l *Logger) Fatal(args ...any) { func (l *Logger) Fatal(args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -474,7 +495,7 @@ func (l *Logger) Fatal(args ...any) {
os.Exit(1) os.Exit(1)
} }
l.log(lx.LevelError, lx.ClassText, concatSpaced(args...), nil, true) l.log(lx.LevelError, lx.ClassText, cat.Space(args...), nil, true)
os.Exit(1) os.Exit(1)
} }
@ -486,7 +507,7 @@ func (l *Logger) Fatal(args ...any) {
// logger.Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits // logger.Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
func (l *Logger) Fatalf(format string, args ...any) { func (l *Logger) Fatalf(format string, args ...any) {
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return return
} }
@ -503,7 +524,7 @@ func (l *Logger) Field(fields map[string]interface{}) *FieldBuilder {
fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})} fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})}
// check if suspended // check if suspended
if l.suspend { if l.suspend.Load() {
return fb return fb
} }
@ -524,7 +545,7 @@ func (l *Logger) Field(fields map[string]interface{}) *FieldBuilder {
func (l *Logger) Fields(pairs ...any) *FieldBuilder { func (l *Logger) Fields(pairs ...any) *FieldBuilder {
fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})} fb := &FieldBuilder{logger: l, fields: make(map[string]interface{})}
if l.suspend { if l.suspend.Load() {
return fb return fb
} }
@ -650,7 +671,7 @@ func (l *Logger) Indent(depth int) *Logger {
// logger := New("app").Enable().Style(lx.NestedPath) // logger := New("app").Enable().Style(lx.NestedPath)
// logger.Info("Started") // Output: [app]: INFO: Started // logger.Info("Started") // Output: [app]: INFO: Started
func (l *Logger) Info(args ...any) { func (l *Logger) Info(args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -658,7 +679,7 @@ func (l *Logger) Info(args ...any) {
return return
} }
l.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), nil, false) l.log(lx.LevelInfo, lx.ClassText, cat.Space(args...), nil, false)
} }
// Infof logs a formatted message at Info level, delegating to Info. It is thread-safe. // Infof logs a formatted message at Info level, delegating to Info. It is thread-safe.
@ -667,7 +688,7 @@ func (l *Logger) Info(args ...any) {
// logger := New("app").Enable().Style(lx.NestedPath) // logger := New("app").Enable().Style(lx.NestedPath)
// logger.Infof("Started %s", "now") // Output: [app]: INFO: Started now // logger.Infof("Started %s", "now") // Output: [app]: INFO: Started now
func (l *Logger) Infof(format string, args ...any) { func (l *Logger) Infof(format string, args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -771,12 +792,20 @@ func (l *Logger) mark(skip int, names ...string) {
// // Output: [app] INFO: function executed [duration=~1ms] // // Output: [app] INFO: function executed [duration=~1ms]
func (l *Logger) Measure(fns ...func()) time.Duration { func (l *Logger) Measure(fns ...func()) time.Duration {
start := time.Now() start := time.Now()
// Execute all provided functions
for _, fn := range fns { for _, fn := range fns {
fn() if fn != nil {
fn()
}
} }
duration := time.Since(start) duration := time.Since(start)
l.Fields("duration", duration).Infof("function executed") l.Fields(
"duration_ns", duration.Nanoseconds(),
"duration", duration.String(),
"duration_ms", fmt.Sprintf("%.3fms", float64(duration.Nanoseconds())/1e6),
).Infof("execution completed")
return duration return duration
} }
@ -789,7 +818,7 @@ func (l *Logger) Measure(fns ...func()) time.Duration {
// child := parent.Namespace("child") // child := parent.Namespace("child")
// child.Info("Child log") // Output: [parent/child] INFO: Child log // child.Info("Child log") // Output: [parent/child] INFO: Child log
func (l *Logger) Namespace(name string) *Logger { func (l *Logger) Namespace(name string) *Logger {
if l.suspend { if l.suspend.Load() {
return l return l
} }
@ -897,9 +926,9 @@ func (l *Logger) NamespaceEnabled(relativePath string) bool {
// logger.Panic("Panic error") // Output: [app] ERROR: Panic error [stack=...], then panics // logger.Panic("Panic error") // Output: [app] ERROR: Panic error [stack=...], then panics
func (l *Logger) Panic(args ...any) { func (l *Logger) Panic(args ...any) {
// Build message by concatenating arguments with spaces // Build message by concatenating arguments with spaces
msg := concatSpaced(args...) msg := cat.Space(args...)
if l.suspend { if l.suspend.Load() {
panic(msg) panic(msg)
} }
@ -942,7 +971,7 @@ func (l *Logger) Prefix(prefix string) *Logger {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Print("message", "value") // Output: [app] INFO: message value // logger.Print("message", "value") // Output: [app] INFO: message value
func (l *Logger) Print(args ...any) { func (l *Logger) Print(args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -950,7 +979,7 @@ func (l *Logger) Print(args ...any) {
if !l.shouldLog(lx.LevelInfo) { if !l.shouldLog(lx.LevelInfo) {
return return
} }
l.log(lx.LevelNone, lx.ClassRaw, concatSpaced(args...), nil, false) l.log(lx.LevelNone, lx.ClassRaw, cat.Space(args...), nil, false)
} }
// Println logs a message at Info level without format specifiers, minimizing allocations // Println logs a message at Info level without format specifiers, minimizing allocations
@ -960,7 +989,7 @@ func (l *Logger) Print(args ...any) {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Println("message", "value") // Output: [app] INFO: message value // logger.Println("message", "value") // Output: [app] INFO: message value
func (l *Logger) Println(args ...any) { func (l *Logger) Println(args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -968,7 +997,7 @@ func (l *Logger) Println(args ...any) {
if !l.shouldLog(lx.LevelInfo) { if !l.shouldLog(lx.LevelInfo) {
return return
} }
l.log(lx.LevelNone, lx.ClassRaw, concatenate(lx.Space, nil, []any{lx.Newline}, args...), nil, false) l.log(lx.LevelNone, lx.ClassRaw, cat.SuffixWith(lx.Space, lx.Newline, args...), nil, false)
} }
// Printf logs a formatted message at Info level, delegating to Print. It is thread-safe. // Printf logs a formatted message at Info level, delegating to Print. It is thread-safe.
@ -977,7 +1006,7 @@ func (l *Logger) Println(args ...any) {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Printf("Message %s", "value") // Output: [app] INFO: Message value // logger.Printf("Message %s", "value") // Output: [app] INFO: Message value
func (l *Logger) Printf(format string, args ...any) { func (l *Logger) Printf(format string, args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -1004,9 +1033,7 @@ func (l *Logger) Remove(m *Middleware) {
// logger.Resume() // logger.Resume()
// logger.Info("Resumed") // Output: [app] INFO: Resumed // logger.Info("Resumed") // Output: [app] INFO: Resumed
func (l *Logger) Resume() *Logger { func (l *Logger) Resume() *Logger {
l.mu.Lock() l.suspend.Store(false)
defer l.mu.Unlock()
l.suspend = false // Clear suspend flag to resume logging
return l return l
} }
@ -1032,9 +1059,7 @@ func (l *Logger) Separator(separator string) *Logger {
// logger.Suspend() // logger.Suspend()
// logger.Info("Ignored") // No output // logger.Info("Ignored") // No output
func (l *Logger) Suspend() *Logger { func (l *Logger) Suspend() *Logger {
l.mu.Lock() l.suspend.Store(true)
defer l.mu.Unlock()
l.suspend = true // Set suspend flag to pause logging
return l return l
} }
@ -1047,9 +1072,7 @@ func (l *Logger) Suspend() *Logger {
// fmt.Println("Logging is suspended") // Prints message // fmt.Println("Logging is suspended") // Prints message
// } // }
func (l *Logger) Suspended() bool { func (l *Logger) Suspended() bool {
l.mu.Lock() return l.suspend.Load()
defer l.mu.Unlock()
return l.suspend // Return current suspend state
} }
// Stack logs messages at Error level with a stack trace for each provided argument. // Stack logs messages at Error level with a stack trace for each provided argument.
@ -1059,7 +1082,7 @@ func (l *Logger) Suspended() bool {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] // logger.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...]
func (l *Logger) Stack(args ...any) { func (l *Logger) Stack(args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -1069,7 +1092,7 @@ func (l *Logger) Stack(args ...any) {
} }
for _, arg := range args { for _, arg := range args {
l.log(lx.LevelError, lx.ClassText, concat(arg), nil, true) l.log(lx.LevelError, lx.ClassText, cat.Concat(arg), nil, true)
} }
} }
@ -1080,7 +1103,7 @@ func (l *Logger) Stack(args ...any) {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...] // logger.Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...]
func (l *Logger) Stackf(format string, args ...any) { func (l *Logger) Stackf(format string, args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -1171,7 +1194,7 @@ func (l *Logger) Use(fn lx.Handler) *Middleware {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Warn("Warning") // Output: [app] WARN: Warning // logger.Warn("Warning") // Output: [app] WARN: Warning
func (l *Logger) Warn(args ...any) { func (l *Logger) Warn(args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -1180,7 +1203,7 @@ func (l *Logger) Warn(args ...any) {
return return
} }
l.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), nil, false) l.log(lx.LevelWarn, lx.ClassText, cat.Space(args...), nil, false)
} }
// Warnf logs a formatted message at Warn level, delegating to Warn. It is thread-safe. // Warnf logs a formatted message at Warn level, delegating to Warn. It is thread-safe.
@ -1189,7 +1212,7 @@ func (l *Logger) Warn(args ...any) {
// logger := New("app").Enable() // logger := New("app").Enable()
// logger.Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued // logger.Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued
func (l *Logger) Warnf(format string, args ...any) { func (l *Logger) Warnf(format string, args ...any) {
if l.suspend { if l.suspend.Load() {
return return
} }
@ -1363,7 +1386,7 @@ func (l *Logger) shouldLog(level lx.LevelType) bool {
} }
// check for suspend mode // check for suspend mode
if l.suspend { if l.suspend.Load() {
return false return false
} }

View file

@ -1,6 +1,7 @@
package lx package lx
import ( import (
"strings"
"time" "time"
) )
@ -31,11 +32,28 @@ const (
// These constants define the severity levels for log messages, used to filter logs based // These constants define the severity levels for log messages, used to filter logs based
// on the loggers minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn). // on the loggers minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn).
const ( const (
LevelNone LevelType = iota // Debug level for detailed diagnostic information LevelNone LevelType = iota // Debug level for detailed diagnostic information
LevelInfo // Info level for general operational messages LevelInfo // Info level for general operational messages
LevelWarn // Warn level for warning conditions LevelWarn // Warn level for warning conditions
LevelError // Error level for error conditions requiring attention LevelError // Error level for error conditions requiring attention
LevelDebug // None level for logs without a specific severity (e.g., raw output) LevelDebug // None level for logs without a specific severity (e.g., raw output)
LevelUnknown // None level for logs without a specific severity (e.g., raw output)
)
// String constants for each level
const (
DebugString = "DEBUG"
InfoString = "INFO"
WarnString = "WARN"
ErrorString = "ERROR"
NoneString = "NONE"
UnknownString = "UNKNOWN"
TextString = "TEXT"
JSONString = "JSON"
DumpString = "DUMP"
SpecialString = "SPECIAL"
RawString = "RAW"
) )
// Log class constants, defining the type of log entry. // Log class constants, defining the type of log entry.
@ -47,6 +65,7 @@ const (
ClassDump // Dump entries for hex/ASCII dumps ClassDump // Dump entries for hex/ASCII dumps
ClassSpecial // Special entries for custom or non-standard logs ClassSpecial // Special entries for custom or non-standard logs
ClassRaw // Raw entries for unformatted output ClassRaw // Raw entries for unformatted output
ClassUnknown // Raw entries for unformatted output
) )
// Namespace style constants. // Namespace style constants.
@ -72,17 +91,37 @@ type LevelType int
func (l LevelType) String() string { func (l LevelType) String() string {
switch l { switch l {
case LevelDebug: case LevelDebug:
return "DEBUG" return DebugString
case LevelInfo: case LevelInfo:
return "INFO" return InfoString
case LevelWarn: case LevelWarn:
return "WARN" return WarnString
case LevelError: case LevelError:
return "ERROR" return ErrorString
case LevelNone: case LevelNone:
return "NONE" return NoneString
default: default:
return "UNKNOWN" return UnknownString
}
}
// LevelParse converts a string to its corresponding LevelType.
// It parses a string (case-insensitive) and returns the corresponding LevelType, defaulting to
// LevelUnknown for unrecognized strings. Supports "WARNING" as an alias for "WARN".
func LevelParse(s string) LevelType {
switch strings.ToUpper(s) {
case DebugString:
return LevelDebug
case InfoString:
return LevelInfo
case WarnString, "WARNING": // Allow both "WARN" and "WARNING"
return LevelWarn
case ErrorString:
return LevelError
case NoneString:
return LevelNone
default:
return LevelUnknown
} }
} }
@ -149,16 +188,36 @@ type ClassType int
func (t ClassType) String() string { func (t ClassType) String() string {
switch t { switch t {
case ClassText: case ClassText:
return "TEST" // Note: Likely a typo, should be "TEXT" return TextString
case ClassJSON: case ClassJSON:
return "JSON" return JSONString
case ClassDump: case ClassDump:
return "DUMP" return DumpString
case ClassSpecial: case ClassSpecial:
return "SPECIAL" return SpecialString
case ClassRaw: case ClassRaw:
return "RAW" return RawString
default: default:
return "UNKNOWN" return UnknownString
}
}
// ParseClass converts a string to its corresponding ClassType.
// It parses a string (case-insensitive) and returns the corresponding ClassType, defaulting to
// ClassUnknown for unrecognized strings.
func ParseClass(s string) ClassType {
switch strings.ToUpper(s) {
case TextString:
return ClassText
case JSONString:
return ClassJSON
case DumpString:
return ClassDump
case SpecialString:
return ClassSpecial
case RawString:
return ClassRaw
default:
return ClassUnknown
} }
} }

55
vendor/github.com/olekukonko/tablewriter/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,55 @@
# See for configurations: https://golangci-lint.run/usage/configuration/
version: 2
# See: https://golangci-lint.run/usage/formatters/
formatters:
default: none
enable:
- gofmt # https://pkg.go.dev/cmd/gofmt
- gofumpt # https://github.com/mvdan/gofumpt
settings:
gofmt:
simplify: true # Simplify code: gofmt with `-s` option.
gofumpt:
# Module path which contains the source code being formatted.
# Default: ""
module-path: github.com/olekukonko/tablewriter # Should match with module in go.mod
# Choose whether to use the extra rules.
# Default: false
extra-rules: true
# See: https://golangci-lint.run/usage/linters/
linters:
default: none
enable:
- staticcheck
- govet
- gocritic
# - unused # TODO: There are many unused functions, should I directly remove those ?
- ineffassign
- unconvert
- mirror
- usestdlibvars
- loggercheck
- exptostd
- godot
- perfsprint
# See: https://golangci-lint.run/usage/false-positives/
exclusion:
# paths:
# rules:
settings:
staticcheck:
checks:
- all
- "-SA1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon
- "-ST1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon
- "-ST1021" # disabled because it warns to have comment on exported packages
- "-ST1000" # disabled because it warns to have comment on exported functions
- "-ST1020" # disabled because it warns to have at least one file in a package should have a package comment
godot:
period: false

View file

@ -416,6 +416,244 @@ func main() {
</table> </table>
``` ```
#### Custom Invoice Renderer
```go
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
)
// InvoiceRenderer implements tw.Renderer for a basic invoice style.
type InvoiceRenderer struct {
writer io.Writer
logger *ll.Logger
rendition tw.Rendition
}
func NewInvoiceRenderer() *InvoiceRenderer {
rendition := tw.Rendition{
Borders: tw.BorderNone,
Symbols: tw.NewSymbols(tw.StyleNone),
Settings: tw.Settings{Separators: tw.SeparatorsNone, Lines: tw.LinesNone},
Streaming: false,
}
defaultLogger := ll.New("simple-invoice-renderer")
return &InvoiceRenderer{logger: defaultLogger, rendition: rendition}
}
func (r *InvoiceRenderer) Logger(logger *ll.Logger) {
if logger != nil {
r.logger = logger
}
}
func (r *InvoiceRenderer) Config() tw.Rendition {
return r.rendition
}
func (r *InvoiceRenderer) Start(w io.Writer) error {
r.writer = w
r.logger.Debug("InvoiceRenderer: Start")
return nil
}
func (r *InvoiceRenderer) formatLine(cells []string, widths tw.Mapper[int, int], cellContexts map[int]tw.CellContext) string {
var sb strings.Builder
numCols := 0
if widths != nil { // Ensure widths is not nil before calling Len
numCols = widths.Len()
}
for i := 0; i < numCols; i++ {
data := ""
if i < len(cells) {
data = cells[i]
}
width := 0
if widths != nil { // Check again before Get
width = widths.Get(i)
}
align := tw.AlignDefault
if cellContexts != nil { // Check cellContexts
if ctx, ok := cellContexts[i]; ok {
align = ctx.Align
}
}
paddedCell := tw.Pad(data, " ", width, align)
sb.WriteString(paddedCell)
if i < numCols-1 {
sb.WriteString(" ") // Column separator
}
}
return sb.String()
}
func (r *InvoiceRenderer) Header(headers [][]string, ctx tw.Formatting) {
if r.writer == nil {
return
}
r.logger.Debugf("InvoiceRenderer: Header (lines: %d)", len(headers))
for _, headerLineCells := range headers {
lineStr := r.formatLine(headerLineCells, ctx.Row.Widths, ctx.Row.Current)
fmt.Fprintln(r.writer, lineStr)
}
if len(headers) > 0 {
totalWidth := 0
if ctx.Row.Widths != nil {
ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w })
if ctx.Row.Widths.Len() > 1 {
totalWidth += (ctx.Row.Widths.Len() - 1) * 3 // Separator spaces
}
}
if totalWidth > 0 {
fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth))
}
}
}
func (r *InvoiceRenderer) Row(row []string, ctx tw.Formatting) {
if r.writer == nil {
return
}
r.logger.Debug("InvoiceRenderer: Row")
lineStr := r.formatLine(row, ctx.Row.Widths, ctx.Row.Current)
fmt.Fprintln(r.writer, lineStr)
}
func (r *InvoiceRenderer) Footer(footers [][]string, ctx tw.Formatting) {
if r.writer == nil {
return
}
r.logger.Debugf("InvoiceRenderer: Footer (lines: %d)", len(footers))
if len(footers) > 0 {
totalWidth := 0
if ctx.Row.Widths != nil {
ctx.Row.Widths.Each(func(_ int, w int) { totalWidth += w })
if ctx.Row.Widths.Len() > 1 {
totalWidth += (ctx.Row.Widths.Len() - 1) * 3
}
}
if totalWidth > 0 {
fmt.Fprintln(r.writer, strings.Repeat("-", totalWidth))
}
}
for _, footerLineCells := range footers {
lineStr := r.formatLine(footerLineCells, ctx.Row.Widths, ctx.Row.Current)
fmt.Fprintln(r.writer, lineStr)
}
}
func (r *InvoiceRenderer) Line(ctx tw.Formatting) {
r.logger.Debug("InvoiceRenderer: Line (no-op)")
// This simple renderer draws its own lines in Header/Footer.
}
func (r *InvoiceRenderer) Close() error {
r.logger.Debug("InvoiceRenderer: Close")
r.writer = nil
return nil
}
func main() {
data := [][]string{
{"Product A", "2", "10.00", "20.00"},
{"Super Long Product Name B", "1", "125.50", "125.50"},
{"Item C", "10", "1.99", "19.90"},
}
header := []string{"Description", "Qty", "Unit Price", "Total Price"}
footer := []string{"", "", "Subtotal:\nTax (10%):\nGRAND TOTAL:", "165.40\n16.54\n181.94"}
invoiceRenderer := NewInvoiceRenderer()
// Create table and set custom renderer using Options
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(invoiceRenderer),
tablewriter.WithAlignment([]tw.Align{
tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight,
}),
)
table.Header(header)
for _, v := range data {
table.Append(v)
}
// Use the Footer method with strings containing newlines for multi-line cells
table.Footer(footer)
fmt.Println("Rendering with InvoiceRenderer:")
table.Render()
// For comparison, render with default Blueprint renderer
// Re-create the table or reset it to use a different renderer
table2 := tablewriter.NewTable(os.Stdout,
tablewriter.WithAlignment([]tw.Align{
tw.AlignLeft, tw.AlignCenter, tw.AlignRight, tw.AlignRight,
}),
)
table2.Header(header)
for _, v := range data {
table2.Append(v)
}
table2.Footer(footer)
fmt.Println("\nRendering with Default Blueprint Renderer (for comparison):")
table2.Render()
}
```
```
Rendering with InvoiceRenderer:
DESCRIPTION QTY UNIT PRICE TOTAL PRICE
--------------------------------------------------------------------
Product A 2 10.00 20.00
Super Long Product Name B 1 125.50 125.50
Item C 10 1.99 19.90
--------------------------------------------------------------------
Subtotal: 165.40
--------------------------------------------------------------------
Tax (10%): 16.54
--------------------------------------------------------------------
GRAND TOTAL: 181.94
```
```
Rendering with Default Blueprint Renderer (for comparison):
┌───────────────────────────┬─────┬──────────────┬─────────────┐
│ DESCRIPTION │ QTY │ UNIT PRICE │ TOTAL PRICE │
├───────────────────────────┼─────┼──────────────┼─────────────┤
│ Product A │ 2 │ 10.00 │ 20.00 │
│ Super Long Product Name B │ 1 │ 125.50 │ 125.50 │
│ Item C │ 10 │ 1.99 │ 19.90 │
├───────────────────────────┼─────┼──────────────┼─────────────┤
│ │ │ Subtotal: │ 165.40 │
│ │ │ Tax (10%): │ 16.54 │
│ │ │ GRAND TOTAL: │ 181.94 │
└───────────────────────────┴─────┴──────────────┴─────────────┘
```
**Notes**: **Notes**:
- The `renderer.NewBlueprint()` is sufficient for most text-based use cases. - The `renderer.NewBlueprint()` is sufficient for most text-based use cases.
- Custom renderers require implementing all interface methods to handle table structure correctly. `tw.Formatting` (which includes `tw.RowContext`) provides cell content and metadata. - Custom renderers require implementing all interface methods to handle table structure correctly. `tw.Formatting` (which includes `tw.RowContext`) provides cell content and metadata.
@ -1914,7 +2152,7 @@ func main() {
- **Direct ANSI Codes**: Embed codes (e.g., `\033[32m` for green) in strings for manual control (`zoo.go:convertCellsToStrings`). - **Direct ANSI Codes**: Embed codes (e.g., `\033[32m` for green) in strings for manual control (`zoo.go:convertCellsToStrings`).
- **tw.Formatter**: Implement `Format() string` on custom types to control cell output, including colors (`tw/types.go:Formatter`). - **tw.Formatter**: Implement `Format() string` on custom types to control cell output, including colors (`tw/types.go:Formatter`).
- **tw.CellFilter**: Use `Config.<Section>.Filter.Global` or `PerColumn` to apply transformations like coloring dynamically (`tw/cell.go:CellFilter`). - **tw.CellFilter**: Use `Config.<Section>.Filter.Global` or `PerColumn` to apply transformations like coloring dynamically (`tw/cell.go:CellFilter`).
- **Width Handling**: `tw.DisplayWidth()` correctly calculates display width of ANSI-coded strings, ignoring escape sequences (`tw/fn.go:DisplayWidth`). - **Width Handling**: `twdw.Width()` correctly calculates display width of ANSI-coded strings, ignoring escape sequences (`tw/fn.go:DisplayWidth`).
- **No Built-In Color Presets**: Unlike v0.0.5s potential `tablewriter.Colors`, v1.0.x requires manual ANSI code management or external libraries for color constants. - **No Built-In Color Presets**: Unlike v0.0.5s potential `tablewriter.Colors`, v1.0.x requires manual ANSI code management or external libraries for color constants.
**Migration Tips**: **Migration Tips**:
@ -1928,7 +2166,7 @@ func main() {
**Potential Pitfalls**: **Potential Pitfalls**:
- **Terminal Support**: Some terminals may not support ANSI codes, causing artifacts; test in your environment or provide a non-colored fallback. - **Terminal Support**: Some terminals may not support ANSI codes, causing artifacts; test in your environment or provide a non-colored fallback.
- **Filter Overlap**: Combining `tw.CellFilter` with `AutoFormat` or other transformations can lead to unexpected results; prioritize filters for coloring (`zoo.go`). - **Filter Overlap**: Combining `tw.CellFilter` with `AutoFormat` or other transformations can lead to unexpected results; prioritize filters for coloring (`zoo.go`).
- **Width Miscalculation**: Incorrect ANSI code handling (e.g., missing `Reset`) can skew width calculations; use `tw.DisplayWidth` (`tw/fn.go`). - **Width Miscalculation**: Incorrect ANSI code handling (e.g., missing `Reset`) can skew width calculations; use `twdw.Width` (`tw/fn.go`).
- **Streaming Consistency**: In streaming mode, ensure color codes are applied consistently across rows to avoid visual discrepancies (`stream.go`). - **Streaming Consistency**: In streaming mode, ensure color codes are applied consistently across rows to avoid visual discrepancies (`stream.go`).
- **Performance**: Applying filters to large datasets may add overhead; optimize filter logic for efficiency (`zoo.go`). - **Performance**: Applying filters to large datasets may add overhead; optimize filter logic for efficiency (`zoo.go`).
@ -2818,7 +3056,7 @@ func main() {
**Notes**: **Notes**:
- **Configuration**: Uses `tw.CellFilter` for per-column coloring, embedding ANSI codes (`tw/cell.go`). - **Configuration**: Uses `tw.CellFilter` for per-column coloring, embedding ANSI codes (`tw/cell.go`).
- **Migration from v0.0.5**: Replaces `SetColumnColor` with dynamic filters (`tablewriter.go`). - **Migration from v0.0.5**: Replaces `SetColumnColor` with dynamic filters (`tablewriter.go`).
- **Key Features**: Flexible color application; `tw.DisplayWidth` handles ANSI codes correctly (`tw/fn.go`). - **Key Features**: Flexible color application; `twdw.Width` handles ANSI codes correctly (`tw/fn.go`).
- **Best Practices**: Test in ANSI-compatible terminals; use constants for code clarity. - **Best Practices**: Test in ANSI-compatible terminals; use constants for code clarity.
- **Potential Issues**: Non-ANSI terminals may show artifacts; provide fallbacks (`tw/fn.go`). - **Potential Issues**: Non-ANSI terminals may show artifacts; provide fallbacks (`tw/fn.go`).
@ -3005,7 +3243,7 @@ This section addresses common migration issues with detailed solutions, covering
| Merging not working | **Cause**: Streaming mode or mismatched data. **Solution**: Use batch mode for vertical/hierarchical merging; ensure identical content (`zoo.go`). | | Merging not working | **Cause**: Streaming mode or mismatched data. **Solution**: Use batch mode for vertical/hierarchical merging; ensure identical content (`zoo.go`). |
| Alignment ignored | **Cause**: `PerColumn` overrides `Global`. **Solution**: Check `Config.Section.Alignment.PerColumn` settings or `ConfigBuilder` calls (`tw/cell.go`). | | Alignment ignored | **Cause**: `PerColumn` overrides `Global`. **Solution**: Check `Config.Section.Alignment.PerColumn` settings or `ConfigBuilder` calls (`tw/cell.go`). |
| Padding affects widths | **Cause**: Padding included in `Config.Widths`. **Solution**: Adjust `Config.Widths` to account for `tw.CellPadding` (`zoo.go`). | | Padding affects widths | **Cause**: Padding included in `Config.Widths`. **Solution**: Adjust `Config.Widths` to account for `tw.CellPadding` (`zoo.go`). |
| Colors not rendering | **Cause**: Non-ANSI terminal or incorrect codes. **Solution**: Test in ANSI-compatible terminal; use `tw.DisplayWidth` (`tw/fn.go`). | | Colors not rendering | **Cause**: Non-ANSI terminal or incorrect codes. **Solution**: Test in ANSI-compatible terminal; use `twdw.Width` (`tw/fn.go`). |
| Caption missing | **Cause**: `Close()` not called in streaming or incorrect `Spot`. **Solution**: Ensure `Close()`; verify `tw.Caption.Spot` (`tablewriter.go`). | | Caption missing | **Cause**: `Close()` not called in streaming or incorrect `Spot`. **Solution**: Ensure `Close()`; verify `tw.Caption.Spot` (`tablewriter.go`). |
| Filters not applied | **Cause**: Incorrect `PerColumn` indexing or nil filters. **Solution**: Set filters correctly; test with sample data (`tw/cell.go`). | | Filters not applied | **Cause**: Incorrect `PerColumn` indexing or nil filters. **Solution**: Set filters correctly; test with sample data (`tw/cell.go`). |
| Stringer cache overhead | **Cause**: Large datasets with diverse types. **Solution**: Disable `WithStringerCache` for small tables if not using `WithStringer` or if types vary greatly (`tablewriter.go`). | | Stringer cache overhead | **Cause**: Large datasets with diverse types. **Solution**: Disable `WithStringerCache` for small tables if not using `WithStringer` or if types vary greatly (`tablewriter.go`). |

View file

@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version #### Latest Version
The latest stable version The latest stable version
```bash ```bash
go get github.com/olekukonko/tablewriter@v1.0.7 go get github.com/olekukonko/tablewriter@v1.1.0
``` ```
**Warning:** Version `v1.0.0` contains missing functionality and should not be used. **Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@ -62,7 +62,7 @@ func main() {
data := [][]string{ data := [][]string{
{"Package", "Version", "Status"}, {"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"}, {"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.0.7", "latest"}, {"tablewriter", "v1.1.0", "latest"},
} }
table := tablewriter.NewWriter(os.Stdout) table := tablewriter.NewWriter(os.Stdout)
@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │ │ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤ ├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │ │ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.0.7 │ latest │ │ tablewriter │ v1.1.0 │ latest │
└─────────────┴─────────┴────────┘ └─────────────┴─────────┴────────┘
``` ```
@ -86,6 +86,43 @@ func main() {
Create a table with `NewTable` or `NewWriter`, configure it using options or a `Config` struct, add data with `Append` or `Bulk`, and render to an `io.Writer`. Use renderers like `Blueprint` (ASCII), `HTML`, `Markdown`, `Colorized`, or `Ocean` (streaming). Create a table with `NewTable` or `NewWriter`, configure it using options or a `Config` struct, add data with `Append` or `Bulk`, and render to an `io.Writer`. Use renderers like `Blueprint` (ASCII), `HTML`, `Markdown`, `Colorized`, or `Ocean` (streaming).
Here's how the API primitives map to the generated ASCII table:
```
API Call ASCII Table Component
-------- ---------------------
table.Header([]string{"NAME", "AGE"}) ┌──────┬─────┐ ← Borders.Top
│ NAME │ AGE │ ← Header row
├──────┼─────┤ ← Lines.ShowTop (header separator)
table.Append([]string{"Alice", "25"}) │ Alice│ 25 │ ← Data row
├──────┼─────┤ ← Separators.BetweenRows
table.Append([]string{"Bob", "30"}) │ Bob │ 30 │ ← Data row
├──────┼─────┤ ← Lines.ShowBottom (footer separator)
table.Footer([]string{"Total", "2"}) │ Total│ 2 │ ← Footer row
└──────┴─────┘ ← Borders.Bottom
```
The core components include:
- **Renderer** - Implements the core interface for converting table data into output formats. Available renderers include Blueprint (ASCII), HTML, Markdown, Colorized (ASCII with color), Ocean (streaming ASCII), and SVG.
- **Config** - The root configuration struct that controls all table behavior and appearance
- **Behavior** - Controls high-level rendering behaviors including auto-hiding empty columns, trimming row whitespace, header/footer visibility, and compact mode for optimized merged cell calculations
- **CellConfig** - The comprehensive configuration template used for table sections (header, row, footer). Combines formatting, padding, alignment, filtering, callbacks, and width constraints with global and per-column control
- **StreamConfig** - Configuration for streaming mode including enable/disable state and strict column validation
- **Rendition** - Defines how a renderer formats tables and contains the complete visual styling configuration
- **Borders** - Control the outer frame visibility (top, bottom, left, right edges) of the table
- **Lines** - Control horizontal boundary lines (above/below headers, above footers) that separate different table sections
- **Separators** - Control the visibility of separators between rows and between columns within the table content
- **Symbols** - Define the characters used for drawing table borders, corners, and junctions
These components can be configured with various `tablewriter.With*()` functional options when creating a new table.
## Examples ## Examples
### Basic Examples ### Basic Examples
@ -1044,6 +1081,8 @@ func (t Time) Format() string {
- `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261) - `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261)
## What is new
- `Counting` changes See [#294](https://github.com/olekukonko/tablewriter/issues/294)
## Command-Line Tool ## Command-Line Tool

View file

@ -14,6 +14,7 @@ type Config struct {
Stream tw.StreamConfig Stream tw.StreamConfig
Behavior tw.Behavior Behavior tw.Behavior
Widths tw.CellWidth Widths tw.CellWidth
Counter tw.Counter
} }
// ConfigBuilder provides a fluent interface for building Config // ConfigBuilder provides a fluent interface for building Config
@ -199,11 +200,7 @@ func (b *ConfigBuilder) WithHeaderMergeMode(mergeMode int) *ConfigBuilder {
// WithMaxWidth sets the maximum width for the entire table (0 means unlimited). // WithMaxWidth sets the maximum width for the entire table (0 means unlimited).
// Negative values are treated as 0. // Negative values are treated as 0.
func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder { func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder {
if width < 0 { b.config.MaxWidth = max(width, 0)
b.config.MaxWidth = 0
} else {
b.config.MaxWidth = width
}
return b return b
} }
@ -688,6 +685,12 @@ func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfi
return bb return bb
} }
// WithAutoHeader enables/disables automatic header extraction for structs in Bulk.
func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder {
bb.config.Structs.AutoHeader = state
return bb
}
// ColumnConfigBuilder configures column-specific settings // ColumnConfigBuilder configures column-specific settings
type ColumnConfigBuilder struct { type ColumnConfigBuilder struct {
parent *ConfigBuilder parent *ConfigBuilder

View file

@ -1,6 +1,8 @@
package tablewriter package tablewriter
import "github.com/olekukonko/tablewriter/tw" import (
"github.com/olekukonko/tablewriter/tw"
)
// WithBorders configures the table's border settings by updating the renderer's border configuration. // WithBorders configures the table's border settings by updating the renderer's border configuration.
// This function is deprecated and will be removed in a future version. // This function is deprecated and will be removed in a future version.

View file

@ -1,9 +1,12 @@
package tablewriter package tablewriter
import ( import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
"reflect" "reflect"
"github.com/mattn/go-runewidth"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
) )
// Option defines a function type for configuring a Table instance. // Option defines a function type for configuring a Table instance.
@ -496,6 +499,17 @@ func WithTrimSpace(state tw.State) Option {
} }
} }
// WithTrimLine sets whether empty visual lines within a cell are trimmed.
// Logs the change if debugging is enabled.
func WithTrimLine(state tw.State) Option {
return func(target *Table) {
target.config.Behavior.TrimLine = state
if target.logger != nil {
target.logger.Debugf("Option: WithTrimLine applied to Table: %v", state)
}
}
}
// WithHeaderAutoFormat enables or disables automatic formatting for header cells. // WithHeaderAutoFormat enables or disables automatic formatting for header cells.
// Logs the change if debugging is enabled. // Logs the change if debugging is enabled.
func WithHeaderAutoFormat(state tw.State) Option { func WithHeaderAutoFormat(state tw.State) Option {
@ -605,14 +619,37 @@ func WithRendition(rendition tw.Rendition) Option {
if target.logger != nil { if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition) target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition)
} }
} else { } else if target.logger != nil {
if target.logger != nil { target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
} }
} }
} }
// WithEastAsian configures the global East Asian width calculation setting.
// - enable=true: Enables East Asian width calculations. CJK and ambiguous characters
// are typically measured as double width.
// - enable=false: Disables East Asian width calculations. Characters are generally
// measured as single width, subject to Unicode standards.
//
// This setting affects all subsequent display width calculations using the twdw package.
func WithEastAsian(enable bool) Option {
return func(target *Table) {
twwidth.SetEastAsian(enable)
}
}
// WithCondition provides a way to set a custom global runewidth.Condition
// that will be used for all subsequent display width calculations by the twwidth (twdw) package.
//
// The runewidth.Condition object allows for more fine-grained control over how rune widths
// are determined, beyond just toggling EastAsianWidth. This could include settings for
// ambiguous width characters or other future properties of runewidth.Condition.
func WithCondition(condition *runewidth.Condition) Option {
return func(target *Table) {
twwidth.SetCondition(condition)
}
}
// WithSymbols sets the symbols used for drawing table borders and separators. // WithSymbols sets the symbols used for drawing table borders and separators.
// The symbols are applied to the table's renderer configuration, if a renderer is set. // The symbols are applied to the table's renderer configuration, if a renderer is set.
// If no renderer is set (target.renderer is nil), this option has no effect. . // If no renderer is set (target.renderer is nil), this option has no effect. .
@ -627,15 +664,38 @@ func WithSymbols(symbols tw.Symbols) Option {
if target.logger != nil { if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg) target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg)
} }
} else { } else if target.logger != nil {
if target.logger != nil { target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
} }
} }
} }
} }
// WithCounters enables line counting by wrapping the table's writer.
// If a custom counter (that implements tw.Counter) is provided, it will be used.
// If the provided counter is nil, a default tw.LineCounter will be used.
// The final count can be retrieved via the table.Lines() method after Render() is called.
func WithCounters(counters ...tw.Counter) Option {
return func(target *Table) {
// Iterate through the provided counters and add any non-nil ones.
for _, c := range counters {
if c != nil {
target.counters = append(target.counters, c)
}
}
}
}
// WithLineCounter enables the default line counter.
// A new instance of tw.LineCounter is added to the table's list of counters.
// The total count can be retrieved via the table.Lines() method after Render() is called.
func WithLineCounter() Option {
return func(target *Table) {
// Important: Create a new instance so tables don't share counters.
target.counters = append(target.counters, &tw.LineCounter{})
}
}
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior. // defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
func defaultConfig() Config { func defaultConfig() Config {
return Config{ return Config{
@ -682,11 +742,19 @@ func defaultConfig() Config {
PerColumn: []tw.Align{}, PerColumn: []tw.Align{},
}, },
}, },
Stream: tw.StreamConfig{}, Stream: tw.StreamConfig{
Debug: false, Enable: false,
StrictColumns: false,
},
Debug: false,
Behavior: tw.Behavior{ Behavior: tw.Behavior{
AutoHide: tw.Off, AutoHide: tw.Off,
TrimSpace: tw.On, TrimSpace: tw.On,
TrimLine: tw.On,
Structs: tw.Struct{
AutoHeader: tw.Off,
Tags: []string{"json", "db"},
},
}, },
} }
} }
@ -814,6 +882,14 @@ func mergeConfig(dst, src Config) Config {
dst.Behavior.Compact = src.Behavior.Compact dst.Behavior.Compact = src.Behavior.Compact
dst.Behavior.Header = src.Behavior.Header dst.Behavior.Header = src.Behavior.Header
dst.Behavior.Footer = src.Behavior.Footer dst.Behavior.Footer = src.Behavior.Footer
dst.Behavior.Footer = src.Behavior.Footer
dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader
// check lent of tags
if len(src.Behavior.Structs.Tags) > 0 {
dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags
}
if src.Widths.Global != 0 { if src.Widths.Global != 0 {
dst.Widths.Global = src.Widths.Global dst.Widths.Global = src.Widths.Global
@ -842,6 +918,8 @@ func mergeStreamConfig(dst, src tw.StreamConfig) tw.StreamConfig {
if src.Enable { if src.Enable {
dst.Enable = true dst.Enable = true
} }
dst.StrictColumns = src.StrictColumns
return dst return dst
} }

View file

@ -8,12 +8,13 @@
package twwarp package twwarp
import ( import (
"github.com/rivo/uniseg"
"math" "math"
"strings" "strings"
"unicode" "unicode"
"github.com/mattn/go-runewidth" "github.com/olekukonko/tablewriter/pkg/twwidth" // IMPORT YOUR NEW PACKAGE
"github.com/rivo/uniseg"
// "github.com/mattn/go-runewidth" // This can be removed if all direct uses are gone
) )
const ( const (
@ -59,7 +60,8 @@ func WrapString(s string, lim int) ([]string, int) {
var lines []string var lines []string
max := 0 max := 0
for _, v := range words { for _, v := range words {
max = runewidth.StringWidth(v) // max = runewidth.StringWidth(v) // OLD
max = twwidth.Width(v) // NEW: Use twdw.Width
if max > lim { if max > lim {
lim = max lim = max
} }
@ -82,12 +84,13 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) {
return []string{""}, lim return []string{""}, lim
} }
if strings.TrimSpace(s) == "" { // All spaces if strings.TrimSpace(s) == "" { // All spaces
if runewidth.StringWidth(s) <= lim { // if runewidth.StringWidth(s) <= lim { // OLD
return []string{s}, runewidth.StringWidth(s) if twwidth.Width(s) <= lim { // NEW: Use twdw.Width
// return []string{s}, runewidth.StringWidth(s) // OLD
return []string{s}, twwidth.Width(s) // NEW: Use twdw.Width
} }
// For very long all-space strings, "wrap" by truncating to the limit. // For very long all-space strings, "wrap" by truncating to the limit.
if lim > 0 { if lim > 0 {
// Use our new helper function to get a substring of the correct display width
substring, _ := stringToDisplayWidth(s, lim) substring, _ := stringToDisplayWidth(s, lim)
return []string{substring}, lim return []string{substring}, lim
} }
@ -96,7 +99,6 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) {
var leadingSpaces, trailingSpaces, coreContent string var leadingSpaces, trailingSpaces, coreContent string
firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
// firstNonSpace will not be -1 due to TrimSpace check above.
leadingSpaces = s[:firstNonSpace] leadingSpaces = s[:firstNonSpace]
lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) }) lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
trailingSpaces = s[lastNonSpace+1:] trailingSpaces = s[lastNonSpace+1:]
@ -116,7 +118,8 @@ func WrapStringWithSpaces(s string, lim int) ([]string, int) {
maxCoreWordWidth := 0 maxCoreWordWidth := 0
for _, v := range words { for _, v := range words {
w := runewidth.StringWidth(v) // w := runewidth.StringWidth(v) // OLD
w := twwidth.Width(v) // NEW: Use twdw.Width
if w > maxCoreWordWidth { if w > maxCoreWordWidth {
maxCoreWordWidth = w maxCoreWordWidth = w
} }
@ -153,15 +156,14 @@ func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWi
g := uniseg.NewGraphemes(s) g := uniseg.NewGraphemes(s)
for g.Next() { for g.Next() {
grapheme := g.Str() grapheme := g.Str()
graphemeWidth := runewidth.StringWidth(grapheme) // Get width of the current grapheme cluster // graphemeWidth := runewidth.StringWidth(grapheme) // OLD
graphemeWidth := twwidth.Width(grapheme) // NEW: Use twdw.Width
if currentWidth+graphemeWidth > targetWidth { if currentWidth+graphemeWidth > targetWidth {
// Adding this grapheme would exceed the target width
break break
} }
currentWidth += graphemeWidth currentWidth += graphemeWidth
// Get the end byte position of the current grapheme cluster
_, e := g.Positions() _, e := g.Positions()
endIndex = e endIndex = e
} }
@ -186,14 +188,15 @@ func WrapWords(words []string, spc, lim, pen int) [][]string {
} }
lengths := make([]int, n) lengths := make([]int, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
lengths[i] = runewidth.StringWidth(words[i]) // lengths[i] = runewidth.StringWidth(words[i]) // OLD
lengths[i] = twwidth.Width(words[i]) // NEW: Use twdw.Width
} }
nbrk := make([]int, n) nbrk := make([]int, n)
cost := make([]int, n) cost := make([]int, n)
for i := range cost { for i := range cost {
cost[i] = math.MaxInt32 cost[i] = math.MaxInt32
} }
remainderLen := lengths[n-1] remainderLen := lengths[n-1] // Uses updated lengths
for i := n - 1; i >= 0; i-- { for i := n - 1; i >= 0; i-- {
if i < n-1 { if i < n-1 {
remainderLen += spc + lengths[i] remainderLen += spc + lengths[i]

View file

@ -0,0 +1,323 @@
package twwidth
import (
"bytes"
"regexp"
"strings"
"sync"
"github.com/mattn/go-runewidth"
)
// condition holds the global runewidth configuration, including East Asian width settings.
var condition *runewidth.Condition
// mu protects access to condition and widthCache for thread safety.
var mu sync.Mutex
// ansi is a compiled regular expression for stripping ANSI escape codes from strings.
var ansi = Filter()
func init() {
condition = runewidth.NewCondition()
widthCache = make(map[cacheKey]int)
}
// cacheKey is used as a key for memoizing string width results in widthCache.
type cacheKey struct {
str string // Input string
eastAsianWidth bool // East Asian width setting
}
// widthCache stores memoized results of Width calculations to improve performance.
var widthCache map[cacheKey]int
// Filter compiles and returns a regular expression for matching ANSI escape sequences,
// including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences.
// The returned regex can be used to strip ANSI codes from strings.
func Filter() *regexp.Regexp {
regESC := "\x1b" // ASCII escape character
regBEL := "\x07" // ASCII bell character
// ANSI string terminator: either ESC+\ or BEL
regST := "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
// Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte
regCSI := regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
// Operating System Command (OSC): ESC] followed by arbitrary content until a terminator
regOSC := regexp.QuoteMeta(regESC+"]") + ".*?" + regST
// Combine CSI and OSC patterns into a single regex
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
}
// SetEastAsian enables or disables East Asian width handling for width calculations.
// When the setting changes, the width cache is cleared to ensure accuracy.
// This function is thread-safe.
//
// Example:
//
// twdw.SetEastAsian(true) // Enable East Asian width handling
func SetEastAsian(enable bool) {
mu.Lock()
defer mu.Unlock()
if condition.EastAsianWidth != enable {
condition.EastAsianWidth = enable
widthCache = make(map[cacheKey]int) // Clear cache on setting change
}
}
// SetCondition updates the global runewidth.Condition used for width calculations.
// When the condition is changed, the width cache is cleared.
// This function is thread-safe.
//
// Example:
//
// newCond := runewidth.NewCondition()
// newCond.EastAsianWidth = true
// twdw.SetCondition(newCond)
func SetCondition(newCond *runewidth.Condition) {
mu.Lock()
defer mu.Unlock()
condition = newCond
widthCache = make(map[cacheKey]int) // Clear cache on setting change
}
// Width calculates the visual width of a string, excluding ANSI escape sequences,
// using the go-runewidth package for accurate Unicode handling. It accounts for the
// current East Asian width setting and caches results for performance.
// This function is thread-safe.
//
// Example:
//
// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10
func Width(str string) int {
mu.Lock()
key := cacheKey{str: str, eastAsianWidth: condition.EastAsianWidth}
if w, found := widthCache[key]; found {
mu.Unlock()
return w
}
mu.Unlock()
// Use a temporary condition to avoid holding the lock during calculation
tempCond := runewidth.NewCondition()
tempCond.EastAsianWidth = key.eastAsianWidth
stripped := ansi.ReplaceAllLiteralString(str, "")
calculatedWidth := tempCond.StringWidth(stripped)
mu.Lock()
widthCache[key] = calculatedWidth
mu.Unlock()
return calculatedWidth
}
// WidthNoCache calculates the visual width of a string without using or
// updating the global cache. It uses the current global East Asian width setting.
// This function is intended for internal use (e.g., benchmarking) and is thread-safe.
//
// Example:
//
// width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10
func WidthNoCache(str string) int {
mu.Lock()
currentEA := condition.EastAsianWidth
mu.Unlock()
tempCond := runewidth.NewCondition()
tempCond.EastAsianWidth = currentEA
stripped := ansi.ReplaceAllLiteralString(str, "")
return tempCond.StringWidth(stripped)
}
// Display calculates the visual width of a string, excluding ANSI escape sequences,
// using the provided runewidth condition. Unlike Width, it does not use caching
// and is intended for cases where a specific condition is required.
// This function is thread-safe with respect to the provided condition.
//
// Example:
//
// cond := runewidth.NewCondition()
// width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10
func Display(cond *runewidth.Condition, str string) int {
return cond.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
}
// Truncate shortens a string to fit within a specified visual width, optionally
// appending a suffix (e.g., "..."). It preserves ANSI escape sequences and adds
// a reset sequence (\x1b[0m) if needed to prevent formatting bleed. The function
// respects the global East Asian width setting and is thread-safe.
//
// If maxWidth is negative, an empty string is returned. If maxWidth is zero and
// a suffix is provided, the suffix is returned. If the string's visual width is
// less than or equal to maxWidth, the string (and suffix, if provided and fits)
// is returned unchanged.
//
// Example:
//
// s := twdw.Truncate("Hello\x1b[31mWorld", 5, "...") // Returns "Hello..."
// s = twdw.Truncate("Hello", 10) // Returns "Hello"
func Truncate(s string, maxWidth int, suffix ...string) string {
if maxWidth < 0 {
return ""
}
suffixStr := strings.Join(suffix, "")
sDisplayWidth := Width(s) // Uses global cached Width
suffixDisplayWidth := Width(suffixStr) // Uses global cached Width
// Case 1: Original string is visually empty.
if sDisplayWidth == 0 {
// If suffix is provided and fits within maxWidth (or if maxWidth is generous)
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
return suffixStr
}
// If s has ANSI codes (len(s)>0) but maxWidth is 0, can't display them.
if maxWidth == 0 && len(s) > 0 {
return ""
}
return s // Returns "" or original ANSI codes
}
// Case 2: maxWidth is 0, but string has content. Cannot display anything.
if maxWidth == 0 {
return ""
}
// Case 3: String fits completely or fits with suffix.
// Here, maxWidth is the total budget for the line.
if sDisplayWidth <= maxWidth {
if len(suffixStr) == 0 { // No suffix.
return s
}
// Suffix is provided. Check if s + suffix fits.
if sDisplayWidth+suffixDisplayWidth <= maxWidth {
return s + suffixStr
}
// s fits, but s + suffix is too long. Return s.
return s
}
// Case 4: String needs truncation (sDisplayWidth > maxWidth).
// maxWidth is the total budget for the final string (content + suffix).
// Capture the global EastAsianWidth setting once for consistent use
mu.Lock()
currentGlobalEastAsianWidth := condition.EastAsianWidth
mu.Unlock()
// Special case for EastAsian true: if only suffix fits, return suffix.
// This was derived from previous test behavior.
if len(suffixStr) > 0 && currentGlobalEastAsianWidth {
provisionalContentWidth := maxWidth - suffixDisplayWidth
if provisionalContentWidth == 0 { // Exactly enough space for suffix only
return suffixStr // <<<< MODIFIED: No ANSI reset here
}
}
// Calculate the budget for the content part, reserving space for the suffix.
targetContentForIteration := maxWidth
if len(suffixStr) > 0 {
targetContentForIteration -= suffixDisplayWidth
}
// If content budget is negative, means not even suffix fits (or no suffix and no space).
// However, if only suffix fits, it should be handled.
if targetContentForIteration < 0 {
// Can we still fit just the suffix?
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
if strings.Contains(s, "\x1b[") {
return "\x1b[0m" + suffixStr
}
return suffixStr
}
return "" // Cannot fit anything.
}
// If targetContentForIteration is 0, loop won't run, result will be empty string, then suffix is added.
var contentBuf bytes.Buffer
var currentContentDisplayWidth int
var ansiSeqBuf bytes.Buffer
inAnsiSequence := false
ansiWrittenToContent := false
localRunewidthCond := runewidth.NewCondition()
localRunewidthCond.EastAsianWidth = currentGlobalEastAsianWidth
for _, r := range s {
if r == '\x1b' {
inAnsiSequence = true
ansiSeqBuf.Reset()
ansiSeqBuf.WriteRune(r)
} else if inAnsiSequence {
ansiSeqBuf.WriteRune(r)
seqBytes := ansiSeqBuf.Bytes()
seqLen := len(seqBytes)
terminated := false
if seqLen >= 2 {
introducer := seqBytes[1]
switch introducer {
case '[':
if seqLen >= 3 && r >= 0x40 && r <= 0x7E {
terminated = true
}
case ']':
if r == '\x07' {
terminated = true
} else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\
terminated = true
}
}
}
if terminated {
inAnsiSequence = false
contentBuf.Write(ansiSeqBuf.Bytes())
ansiWrittenToContent = true
ansiSeqBuf.Reset()
}
} else { // Normal character
runeDisplayWidth := localRunewidthCond.RuneWidth(r)
if targetContentForIteration == 0 { // No budget for content at all
break
}
if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration {
break
}
contentBuf.WriteRune(r)
currentContentDisplayWidth += runeDisplayWidth
}
}
result := contentBuf.String()
// Suffix is added if:
// 1. A suffix string is provided.
// 2. Truncation actually happened (sDisplayWidth > maxWidth originally)
// OR if the content part is empty but a suffix is meant to be shown
// (e.g. targetContentForIteration was 0).
if len(suffixStr) > 0 {
// Add suffix if we are in the truncation path (sDisplayWidth > maxWidth)
// OR if targetContentForIteration was 0 (meaning only suffix should be shown)
// but we must ensure we don't exceed original maxWidth.
// The logic above for targetContentForIteration already ensures space.
needsReset := false
// Condition for reset: if styling was active in 's' and might affect suffix
if (ansiWrittenToContent || (inAnsiSequence && strings.Contains(s, "\x1b["))) && (currentContentDisplayWidth > 0 || ansiWrittenToContent) {
if !strings.HasSuffix(result, "\x1b[0m") {
needsReset = true
}
} else if currentContentDisplayWidth > 0 && strings.Contains(result, "\x1b[") && !strings.HasSuffix(result, "\x1b[0m") && strings.Contains(s, "\x1b[") {
// If result has content and ANSI, and original had ANSI, and result not already reset
needsReset = true
}
if needsReset {
result += "\x1b[0m"
}
result += suffixStr
}
return result
}

View file

@ -1,10 +1,12 @@
package renderer package renderer
import ( import (
"github.com/olekukonko/ll"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -42,7 +44,7 @@ func NewBlueprint(configs ...tw.Rendition) *Blueprint {
// Merge user settings with default settings // Merge user settings with default settings
cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings) cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings)
} }
return &Blueprint{config: cfg} return &Blueprint{config: cfg, logger: ll.New("blueprint")}
} }
// Close performs cleanup (no-op in this implementation). // Close performs cleanup (no-op in this implementation).
@ -106,7 +108,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
} }
if prefix != tw.Empty || suffix != tw.Empty { if prefix != tw.Empty || suffix != tw.Empty {
line.WriteString(prefix + suffix + tw.NewLine) line.WriteString(prefix + suffix + tw.NewLine)
totalLineWidth = tw.DisplayWidth(prefix) + tw.DisplayWidth(suffix) totalLineWidth = twwidth.Width(prefix) + twwidth.Width(suffix)
f.w.Write([]byte(line.String())) f.w.Write([]byte(line.String()))
} }
f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth) f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth)
@ -119,13 +121,13 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
targetTotalWidth += ctx.Row.Widths.Get(colIdx) targetTotalWidth += ctx.Row.Widths.Get(colIdx)
} }
if f.config.Borders.Left.Enabled() { if f.config.Borders.Left.Enabled() {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
} }
if f.config.Borders.Right.Enabled() { if f.config.Borders.Right.Enabled() {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
} }
if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 { if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 {
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column()) * (len(sortedKeys) - 1) targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) * (len(sortedKeys) - 1)
} }
// Add left border if enabled // Add left border if enabled
@ -133,7 +135,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
if f.config.Borders.Left.Enabled() { if f.config.Borders.Left.Enabled() {
leftBorder := jr.RenderLeft() leftBorder := jr.RenderLeft()
line.WriteString(leftBorder) line.WriteString(leftBorder)
leftBorderWidth = tw.DisplayWidth(leftBorder) leftBorderWidth = twwidth.Width(leftBorder)
totalLineWidth += leftBorderWidth totalLineWidth += leftBorderWidth
f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth) f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth)
} }
@ -156,11 +158,11 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
// Adjust colWidth to account for wider borders // Adjust colWidth to account for wider borders
adjustedColWidth := colWidth adjustedColWidth := colWidth
if f.config.Borders.Left.Enabled() && keyIndex == 0 { if f.config.Borders.Left.Enabled() && keyIndex == 0 {
adjustedColWidth -= leftBorderWidth - tw.DisplayWidth(f.config.Symbols.Column()) adjustedColWidth -= leftBorderWidth - twwidth.Width(f.config.Symbols.Column())
} }
if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 { if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 {
rightBorderWidth := tw.DisplayWidth(jr.RenderRight(currentColIdx)) rightBorderWidth := twwidth.Width(jr.RenderRight(currentColIdx))
adjustedColWidth -= rightBorderWidth - tw.DisplayWidth(f.config.Symbols.Column()) adjustedColWidth -= rightBorderWidth - twwidth.Width(f.config.Symbols.Column())
} }
if adjustedColWidth < 0 { if adjustedColWidth < 0 {
adjustedColWidth = 0 adjustedColWidth = 0
@ -172,7 +174,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
totalLineWidth += adjustedColWidth totalLineWidth += adjustedColWidth
f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx) f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx)
} else { } else {
segmentWidth := tw.DisplayWidth(segment) segmentWidth := twwidth.Width(segment)
if segmentWidth == 0 { if segmentWidth == 0 {
segmentWidth = 1 // Avoid division by zero segmentWidth = 1 // Avoid division by zero
f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment) f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment)
@ -183,11 +185,11 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
repeat = 1 repeat = 1
} }
repeatedSegment := strings.Repeat(segment, repeat) repeatedSegment := strings.Repeat(segment, repeat)
actualWidth := tw.DisplayWidth(repeatedSegment) actualWidth := twwidth.Width(repeatedSegment)
if actualWidth > adjustedColWidth { if actualWidth > adjustedColWidth {
// Truncate if too long // Truncate if too long
repeatedSegment = tw.TruncateString(repeatedSegment, adjustedColWidth) repeatedSegment = twwidth.Truncate(repeatedSegment, adjustedColWidth)
actualWidth = tw.DisplayWidth(repeatedSegment) actualWidth = twwidth.Width(repeatedSegment)
f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth) f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth)
} else if actualWidth < adjustedColWidth { } else if actualWidth < adjustedColWidth {
// Pad with segment character to match adjustedColWidth // Pad with segment character to match adjustedColWidth
@ -195,7 +197,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
for i := 0; i < remainingWidth/segmentWidth; i++ { for i := 0; i < remainingWidth/segmentWidth; i++ {
repeatedSegment += segment repeatedSegment += segment
} }
actualWidth = tw.DisplayWidth(repeatedSegment) actualWidth = twwidth.Width(repeatedSegment)
if actualWidth < adjustedColWidth { if actualWidth < adjustedColWidth {
repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth) repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth)
actualWidth = adjustedColWidth actualWidth = adjustedColWidth
@ -214,13 +216,13 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
nextColIdx := visibleColIndices[keyIndex+1] nextColIdx := visibleColIndices[keyIndex+1]
junction := jr.RenderJunction(currentColIdx, nextColIdx) junction := jr.RenderJunction(currentColIdx, nextColIdx)
// Use center symbol (❀) or column separator (|) to match data rows // Use center symbol (❀) or column separator (|) to match data rows
if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) { if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
junction = f.config.Symbols.Center() junction = f.config.Symbols.Center()
if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) { if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
junction = f.config.Symbols.Column() junction = f.config.Symbols.Column()
} }
} }
junctionWidth := tw.DisplayWidth(junction) junctionWidth := twwidth.Width(junction)
line.WriteString(junction) line.WriteString(junction)
totalLineWidth += junctionWidth totalLineWidth += junctionWidth
f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth) f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth)
@ -232,7 +234,7 @@ func (f *Blueprint) Line(ctx tw.Formatting) {
if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 { if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 {
lastIdx := visibleColIndices[len(visibleColIndices)-1] lastIdx := visibleColIndices[len(visibleColIndices)-1]
rightBorder := jr.RenderRight(lastIdx) rightBorder := jr.RenderRight(lastIdx)
rightBorderWidth = tw.DisplayWidth(rightBorder) rightBorderWidth = twwidth.Width(rightBorder)
line.WriteString(rightBorder) line.WriteString(rightBorder)
totalLineWidth += rightBorderWidth totalLineWidth += rightBorderWidth
f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth) f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth)
@ -276,13 +278,13 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
content, width, align, padding.Left, padding.Right) content, width, align, padding.Left, padding.Right)
// Calculate display width of content // Calculate display width of content
runeWidth := tw.DisplayWidth(content) runeWidth := twwidth.Width(content)
// Set default padding characters // Set default padding characters
leftPadChar := padding.Left leftPadChar := padding.Left
rightPadChar := padding.Right rightPadChar := padding.Right
//if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() { // if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
// if leftPadChar == tw.Empty { // if leftPadChar == tw.Empty {
// leftPadChar = tw.Space // leftPadChar = tw.Space
// } // }
@ -292,28 +294,22 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
//} //}
// Calculate padding widths // Calculate padding widths
padLeftWidth := tw.DisplayWidth(leftPadChar) padLeftWidth := twwidth.Width(leftPadChar)
padRightWidth := tw.DisplayWidth(rightPadChar) padRightWidth := twwidth.Width(rightPadChar)
// Calculate available width for content // Calculate available width for content
availableContentWidth := width - padLeftWidth - padRightWidth availableContentWidth := max(width-padLeftWidth-padRightWidth, 0)
if availableContentWidth < 0 {
availableContentWidth = 0
}
f.logger.Debugf("Available content width: %d", availableContentWidth) f.logger.Debugf("Available content width: %d", availableContentWidth)
// Truncate content if it exceeds available width // Truncate content if it exceeds available width
if runeWidth > availableContentWidth { if runeWidth > availableContentWidth {
content = tw.TruncateString(content, availableContentWidth) content = twwidth.Truncate(content, availableContentWidth)
runeWidth = tw.DisplayWidth(content) runeWidth = twwidth.Width(content)
f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth) f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth)
} }
// Calculate total padding needed // Calculate total padding needed
totalPaddingWidth := width - runeWidth totalPaddingWidth := max(width-runeWidth, 0)
if totalPaddingWidth < 0 {
totalPaddingWidth = 0
}
f.logger.Debugf("Total padding width: %d", totalPaddingWidth) f.logger.Debugf("Total padding width: %d", totalPaddingWidth)
var result strings.Builder var result strings.Builder
@ -363,10 +359,10 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
} }
output := result.String() output := result.String()
finalWidth := tw.DisplayWidth(output) finalWidth := twwidth.Width(output)
// Adjust output to match target width // Adjust output to match target width
if finalWidth > width { if finalWidth > width {
output = tw.TruncateString(output, width) output = twwidth.Truncate(output, width)
f.logger.Debugf("formatCell: Truncated output to width %d", width) f.logger.Debugf("formatCell: Truncated output to width %d", width)
} else if finalWidth < width { } else if finalWidth < width {
output = tw.PadRight(output, tw.Space, width) output = tw.PadRight(output, tw.Space, width)
@ -374,9 +370,9 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
} }
// Log warning if final width doesn't match target // Log warning if final width doesn't match target
if f.logger.Enabled() && tw.DisplayWidth(output) != width { if f.logger.Enabled() && twwidth.Width(output) != width {
f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'", f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'",
tw.DisplayWidth(output), width, output) twwidth.Width(output), width, output)
} }
f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width) f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width)
@ -407,14 +403,14 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
totalLineWidth := 0 // Track total display width totalLineWidth := 0 // Track total display width
if prefix != tw.Empty { if prefix != tw.Empty {
output.WriteString(prefix) output.WriteString(prefix)
totalLineWidth += tw.DisplayWidth(prefix) totalLineWidth += twwidth.Width(prefix)
f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, tw.DisplayWidth(prefix)) f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, twwidth.Width(prefix))
} }
colIndex := 0 colIndex := 0
separatorDisplayWidth := 0 separatorDisplayWidth := 0
if f.config.Settings.Separators.BetweenColumns.Enabled() { if f.config.Settings.Separators.BetweenColumns.Enabled() {
separatorDisplayWidth = tw.DisplayWidth(columnSeparator) separatorDisplayWidth = twwidth.Width(columnSeparator)
} }
// Process each column // Process each column
@ -434,7 +430,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
prevWidth := ctx.Row.Widths.Get(colIndex - 1) prevWidth := ctx.Row.Widths.Get(colIndex - 1)
prevCellCtx, prevOk := ctx.Row.Current[colIndex-1] prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End
if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start)) { if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start)) {
shouldAddSeparator = true shouldAddSeparator = true
} }
} }
@ -453,10 +449,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
if ctx.Row.Position == tw.Row { if ctx.Row.Position == tw.Row {
dynamicTotalWidth := 0 dynamicTotalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ { for k := 0; k < span && colIndex+k < numCols; k++ {
normWidth := ctx.NormalizedWidths.Get(colIndex + k) normWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 { if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
dynamicTotalWidth += separatorDisplayWidth dynamicTotalWidth += separatorDisplayWidth
@ -500,21 +493,24 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
// Set cell padding and alignment // Set cell padding and alignment
padding := cellCtx.Padding padding := cellCtx.Padding
align := cellCtx.Align align := cellCtx.Align
if align == tw.AlignNone { switch align {
if ctx.Row.Position == tw.Header { case tw.AlignNone:
switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
align = tw.AlignRight align = tw.AlignRight
} else { default:
align = tw.AlignLeft align = tw.AlignLeft
} }
f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position) f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position)
} else if align == tw.Skip { case tw.Skip:
if ctx.Row.Position == tw.Header { switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
align = tw.AlignRight align = tw.AlignRight
} else { default:
align = tw.AlignLeft align = tw.AlignLeft
} }
f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align) f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align)
@ -522,9 +518,22 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
isTotalPattern := false isTotalPattern := false
// Case-insensitive check for "total"
if isHMergeStart && colIndex > 0 {
if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok {
if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") {
isTotalPattern = true
f.logger.Debugf("renderLine: total pattern in row in %d", colIndex)
}
}
}
// Get the alignment from the configuration
align = cellCtx.Align
// Override alignment for footer merged cells // Override alignment for footer merged cells
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight { if align == tw.AlignNone {
f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align) f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align)
align = tw.AlignRight align = tw.AlignRight
} }
@ -542,7 +551,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
formattedCell := f.formatCell(cellData, visualWidth, padding, align) formattedCell := f.formatCell(cellData, visualWidth, padding, align)
if len(formattedCell) > 0 { if len(formattedCell) > 0 {
output.WriteString(formattedCell) output.WriteString(formattedCell)
cellWidth := tw.DisplayWidth(formattedCell) cellWidth := twwidth.Width(formattedCell)
totalLineWidth += cellWidth totalLineWidth += cellWidth
f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth) f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth)
} }
@ -561,17 +570,18 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
// Add suffix and adjust total width // Add suffix and adjust total width
if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() { if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() {
output.WriteString(suffix) output.WriteString(suffix)
totalLineWidth += tw.DisplayWidth(suffix) totalLineWidth += twwidth.Width(suffix)
f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, tw.DisplayWidth(suffix)) f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, twwidth.Width(suffix))
} }
output.WriteString(tw.NewLine) output.WriteString(tw.NewLine)
f.w.Write([]byte(output.String())) f.w.Write([]byte(output.String()))
f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth) f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth)
} }
// Rendition updates the Blueprint's configuration.
func (f *Blueprint) Rendition(config tw.Rendition) { func (f *Blueprint) Rendition(config tw.Rendition) {
f.config = mergeRendition(f.config, config) f.config = mergeRendition(f.config, config)
f.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", f.config) f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config)
} }
// Ensure Blueprint implements tw.Renditioning // Ensure Blueprint implements tw.Renditioning

View file

@ -1,11 +1,13 @@
package renderer package renderer
import ( import (
"io"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lh"
"io" "github.com/olekukonko/tablewriter/pkg/twwidth"
"strings"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -254,7 +256,7 @@ func (c *Colorized) Line(ctx tw.Formatting) {
line.WriteString(strings.Repeat(tw.Space, colWidth)) line.WriteString(strings.Repeat(tw.Space, colWidth))
} else { } else {
// Calculate how many times to repeat the segment // Calculate how many times to repeat the segment
segmentWidth := tw.DisplayWidth(segment) segmentWidth := twwidth.Width(segment)
if segmentWidth <= 0 { if segmentWidth <= 0 {
segmentWidth = 1 segmentWidth = 1
} }
@ -266,7 +268,7 @@ func (c *Colorized) Line(ctx tw.Formatting) {
line.WriteString(drawnSegment) line.WriteString(drawnSegment)
// Adjust for width discrepancies // Adjust for width discrepancies
actualDrawnWidth := tw.DisplayWidth(drawnSegment) actualDrawnWidth := twwidth.Width(drawnSegment)
if actualDrawnWidth < colWidth { if actualDrawnWidth < colWidth {
missingWidth := colWidth - actualDrawnWidth missingWidth := colWidth - actualDrawnWidth
spaces := strings.Repeat(tw.Space, missingWidth) spaces := strings.Repeat(tw.Space, missingWidth)
@ -373,39 +375,33 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
} }
// Calculate visual width of content // Calculate visual width of content
contentVisualWidth := tw.DisplayWidth(content) contentVisualWidth := twwidth.Width(content)
// Set default padding characters // Set default padding characters
padLeftCharStr := padding.Left padLeftCharStr := padding.Left
if padLeftCharStr == tw.Empty { // if padLeftCharStr == tw.Empty {
padLeftCharStr = tw.Space // padLeftCharStr = tw.Space
} //}
padRightCharStr := padding.Right padRightCharStr := padding.Right
if padRightCharStr == tw.Empty { // if padRightCharStr == tw.Empty {
padRightCharStr = tw.Space // padRightCharStr = tw.Space
} //}
// Calculate padding widths // Calculate padding widths
definedPadLeftWidth := tw.DisplayWidth(padLeftCharStr) definedPadLeftWidth := twwidth.Width(padLeftCharStr)
definedPadRightWidth := tw.DisplayWidth(padRightCharStr) definedPadRightWidth := twwidth.Width(padRightCharStr)
// Calculate available width for content and alignment // Calculate available width for content and alignment
availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth availableForContentAndAlign := max(width-definedPadLeftWidth-definedPadRightWidth, 0)
if availableForContentAndAlign < 0 {
availableForContentAndAlign = 0
}
// Truncate content if it exceeds available width // Truncate content if it exceeds available width
if contentVisualWidth > availableForContentAndAlign { if contentVisualWidth > availableForContentAndAlign {
content = tw.TruncateString(content, availableForContentAndAlign) content = twwidth.Truncate(content, availableForContentAndAlign)
contentVisualWidth = tw.DisplayWidth(content) contentVisualWidth = twwidth.Width(content)
c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth) c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth)
} }
// Calculate remaining space for alignment // Calculate remaining space for alignment
remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth remainingSpaceForAlignment := max(availableForContentAndAlign-contentVisualWidth, 0)
if remainingSpaceForAlignment < 0 {
remainingSpaceForAlignment = 0
}
// Apply alignment padding // Apply alignment padding
leftAlignmentPadSpaces := tw.Empty leftAlignmentPadSpaces := tw.Empty
@ -472,12 +468,12 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
output := sb.String() output := sb.String()
// Adjust output width if necessary // Adjust output width if necessary
currentVisualWidth := tw.DisplayWidth(output) currentVisualWidth := twwidth.Width(output)
if currentVisualWidth != width { if currentVisualWidth != width {
c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'", c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'",
content, width, currentVisualWidth, output) content, width, currentVisualWidth, output)
if currentVisualWidth > width { if currentVisualWidth > width {
output = tw.TruncateString(output, width) output = twwidth.Truncate(output, width)
} else { } else {
paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth) paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth)
if len(tint.BG) > 0 { if len(tint.BG) > 0 {
@ -486,10 +482,10 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
output += paddingSpacesStr output += paddingSpacesStr
} }
} }
c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, tw.DisplayWidth(output), output) c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, twwidth.Width(output), output)
} }
c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, tw.DisplayWidth(output)) c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, twwidth.Width(output))
return output return output
} }
@ -529,7 +525,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
separatorString := tw.Empty separatorString := tw.Empty
if c.config.Settings.Separators.BetweenColumns.Enabled() { if c.config.Settings.Separators.BetweenColumns.Enabled() {
separatorString = c.config.Separator.Apply(c.config.Symbols.Column()) separatorString = c.config.Separator.Apply(c.config.Symbols.Column())
separatorDisplayWidth = tw.DisplayWidth(c.config.Symbols.Column()) separatorDisplayWidth = twwidth.Width(c.config.Symbols.Column())
} }
// Process each column // Process each column
@ -538,7 +534,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
shouldAddSeparator := false shouldAddSeparator := false
if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() { if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
cellCtx, ok := ctx.Row.Current[i] cellCtx, ok := ctx.Row.Current[i]
if !ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start) { if !ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start) {
shouldAddSeparator = true shouldAddSeparator = true
} }
} }
@ -573,10 +569,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
dynamicTotalWidth := 0 dynamicTotalWidth := 0
for k := 0; k < span && i+k < numCols; k++ { for k := 0; k < span && i+k < numCols; k++ {
colToSum := i + k colToSum := i + k
normWidth := ctx.NormalizedWidths.Get(colToSum) normWidth := max(ctx.NormalizedWidths.Get(colToSum), 0)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 { if k > 0 && separatorDisplayWidth > 0 {
dynamicTotalWidth += separatorDisplayWidth dynamicTotalWidth += separatorDisplayWidth
@ -632,7 +625,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
} }
// Override alignment for footer merges or TOTAL pattern // Override alignment for footer merges or TOTAL pattern
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight { if align == tw.AlignNone {
c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align) c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align)
align = tw.AlignRight align = tw.AlignRight
} }
@ -693,8 +686,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
// Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition // Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition
// by merging the provided newRendition. Color-specific Tints are not modified. // by merging the provided newRendition. Color-specific Tints are not modified.
func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface
c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.config.Borders, c.config.Symbols, c.config.Settings, newRendition)
c.config.Borders, c.config.Symbols, c.config.Settings, newRendition)
currentRenditionPart := tw.Rendition{ currentRenditionPart := tw.Rendition{
Borders: c.config.Borders, Borders: c.config.Borders,

View file

@ -2,6 +2,7 @@ package renderer
import ( import (
"fmt" "fmt"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -82,7 +83,6 @@ func defaultColorized() ColorizedConfig {
// defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer. // defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer.
func defaultOceanRendererConfig() tw.Rendition { func defaultOceanRendererConfig() tw.Rendition {
return tw.Rendition{ return tw.Rendition{
Borders: tw.Border{ Borders: tw.Border{
Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On, Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On,
@ -190,7 +190,7 @@ func mergeSettings(defaults, overrides tw.Settings) tw.Settings {
defaults.CompactMode = overrides.CompactMode defaults.CompactMode = overrides.CompactMode
} }
//if overrides.Cushion != tw.Unknown { // if overrides.Cushion != tw.Unknown {
// defaults.Cushion = overrides.Cushion // defaults.Cushion = overrides.Cushion
//} //}

View file

@ -3,11 +3,12 @@ package renderer
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/olekukonko/ll"
"html" "html"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -63,6 +64,7 @@ func NewHTML(configs ...HTMLConfig) *HTML {
tableStarted: false, tableStarted: false,
tbodyStarted: false, tbodyStarted: false,
tfootStarted: false, tfootStarted: false,
logger: ll.New("html"),
} }
} }
@ -81,7 +83,7 @@ func (h *HTML) Config() tw.Rendition {
} }
// debugLog appends a formatted message to the debug trace if debugging is enabled. // debugLog appends a formatted message to the debug trace if debugging is enabled.
//func (h *HTML) debugLog(format string, a ...interface{}) { // func (h *HTML) debugLog(format string, a ...interface{}) {
// if h.debug { // if h.debug {
// msg := fmt.Sprintf(format, a...) // msg := fmt.Sprintf(format, a...)
// h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg)) // h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg))

View file

@ -1,10 +1,12 @@
package renderer package renderer
import ( import (
"github.com/olekukonko/ll"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -35,7 +37,7 @@ func NewMarkdown(configs ...tw.Rendition) *Markdown {
if len(configs) > 0 { if len(configs) > 0 {
cfg = mergeMarkdownConfig(cfg, configs[0]) cfg = mergeMarkdownConfig(cfg, configs[0])
} }
return &Markdown{config: cfg} return &Markdown{config: cfg, logger: ll.New("markdown")}
} }
// mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings. // mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings.
@ -94,7 +96,6 @@ func (m *Markdown) Row(row []string, ctx tw.Formatting) {
m.resolveAlignment(ctx) m.resolveAlignment(ctx)
m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next) m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
m.renderMarkdownLine(row, ctx, false) m.renderMarkdownLine(row, ctx, false)
} }
// Footer renders the Markdown table footer. // Footer renders the Markdown table footer.
@ -154,10 +155,10 @@ func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide. // formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string { func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
//if m.config.Settings.TrimWhitespace.Enabled() { // if m.config.Settings.TrimWhitespace.Enabled() {
// content = strings.TrimSpace(content) // content = strings.TrimSpace(content)
//} //}
contentVisualWidth := tw.DisplayWidth(content) contentVisualWidth := twwidth.Width(content)
// Use specified padding characters or default to spaces // Use specified padding characters or default to spaces
padLeftChar := padding.Left padLeftChar := padding.Left
@ -170,16 +171,13 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
} }
// Calculate padding widths // Calculate padding widths
padLeftCharWidth := tw.DisplayWidth(padLeftChar) padLeftCharWidth := twwidth.Width(padLeftChar)
padRightCharWidth := tw.DisplayWidth(padRightChar) padRightCharWidth := twwidth.Width(padRightChar)
minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth) minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth)
targetWidth := tw.Max(width, minWidth) targetWidth := tw.Max(width, minWidth)
// Calculate padding // Calculate padding
totalPaddingNeeded := targetWidth - contentVisualWidth totalPaddingNeeded := max(targetWidth-contentVisualWidth, 0)
if totalPaddingNeeded < 0 {
totalPaddingNeeded = 0
}
var leftPadStr, rightPadStr string var leftPadStr, rightPadStr string
switch align { switch align {
@ -212,26 +210,27 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
result := leftPadStr + content + rightPadStr result := leftPadStr + content + rightPadStr
// Adjust width if needed // Adjust width if needed
finalWidth := tw.DisplayWidth(result) finalWidth := twwidth.Width(result)
if finalWidth != targetWidth { if finalWidth != targetWidth {
m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d", m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d",
content, targetWidth, padding.Left, padding.Right, align, result, finalWidth) content, targetWidth, padding.Left, padding.Right, align, result, finalWidth)
adjNeeded := targetWidth - finalWidth adjNeeded := targetWidth - finalWidth
if adjNeeded > 0 { if adjNeeded > 0 {
adjStr := strings.Repeat(tw.Space, adjNeeded) adjStr := strings.Repeat(tw.Space, adjNeeded)
if align == tw.AlignRight { switch align {
case tw.AlignRight:
result = adjStr + result result = adjStr + result
} else if align == tw.AlignCenter { case tw.AlignCenter:
leftAdj := adjNeeded / 2 leftAdj := adjNeeded / 2
rightAdj := adjNeeded - leftAdj rightAdj := adjNeeded - leftAdj
result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj) result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
} else { default:
result += adjStr result += adjStr
} }
} else { } else {
result = tw.TruncateString(result, targetWidth) result = twwidth.Truncate(result, targetWidth)
} }
m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, tw.DisplayWidth(result)) m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, twwidth.Width(result))
} }
m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)", m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)",
@ -262,11 +261,11 @@ func (m *Markdown) formatSeparator(width int, align tw.Align) string {
} }
result := sb.String() result := sb.String()
currentLen := tw.DisplayWidth(result) currentLen := twwidth.Width(result)
if currentLen < targetWidth { if currentLen < targetWidth {
result += strings.Repeat("-", targetWidth-currentLen) result += strings.Repeat("-", targetWidth-currentLen)
} else if currentLen > targetWidth { } else if currentLen > targetWidth {
result = tw.TruncateString(result, targetWidth) result = twwidth.Truncate(result, targetWidth)
} }
m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result) m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result)
@ -314,7 +313,7 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
output.WriteString(prefix) output.WriteString(prefix)
colIndex := 0 colIndex := 0
separatorWidth := tw.DisplayWidth(separator) separatorWidth := twwidth.Width(separator)
for colIndex < numCols { for colIndex < numCols {
cellCtx, ok := ctx.Row.Current[colIndex] cellCtx, ok := ctx.Row.Current[colIndex]
@ -345,10 +344,7 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
span = cellCtx.Merge.Horizontal.Span span = cellCtx.Merge.Horizontal.Span
totalWidth := 0 totalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ { for k := 0; k < span && colIndex+k < numCols; k++ {
colWidth := ctx.NormalizedWidths.Get(colIndex + k) colWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
if colWidth < 0 {
colWidth = 0
}
totalWidth += colWidth totalWidth += colWidth
if k > 0 && separatorWidth > 0 { if k > 0 && separatorWidth > 0 {
totalWidth += separatorWidth totalWidth += separatorWidth
@ -387,17 +383,18 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
} }
// For rows, use the header's alignment if specified // For rows, use the header's alignment if specified
rowAlign := align rowAlign := align
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false { if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && !isHeaderSep {
if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty { if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
rowAlign = headerCellCtx.Align rowAlign = headerCellCtx.Align
} }
} }
if rowAlign == tw.AlignNone || rowAlign == tw.Empty { if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
if ctx.Row.Position == tw.Header { switch ctx.Row.Position {
case tw.Header:
rowAlign = tw.AlignCenter rowAlign = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
rowAlign = tw.AlignRight rowAlign = tw.AlignRight
} else { default:
rowAlign = tw.AlignLeft rowAlign = tw.AlignLeft
} }
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign) m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)

View file

@ -2,15 +2,17 @@ package renderer
import ( import (
"io" "io"
"slices"
"strings" "strings"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
// OceanConfig defines configuration specific to the Ocean renderer. // OceanConfig defines configuration specific to the Ocean renderer.
type OceanConfig struct { type OceanConfig struct{}
}
// Ocean is a streaming table renderer that writes ASCII tables. // Ocean is a streaming table renderer that writes ASCII tables.
type Ocean struct { type Ocean struct {
@ -106,17 +108,9 @@ func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) {
if !o.widthsFinalized { if !o.widthsFinalized {
o.tryFinalizeWidths(ctx) o.tryFinalizeWidths(ctx)
} }
// The batch renderer (table.go/renderHeader) will call Line() for the top border
// and for the header separator if its main config t.config says so.
// So, Ocean.Header should *not* draw these itself when in batch mode.
// For true streaming, table.go's streamRenderHeader would make these Line() calls.
// Decision: Ocean.Header *only* renders header content.
// Lines (top border, header separator) are managed by the caller (batch or stream logic in table.go).
if !o.widthsFinalized { if !o.widthsFinalized {
o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.") o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.")
// o.headerContentRendered = true; // No, content wasn't rendered.
return return
} }
@ -133,10 +127,7 @@ func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) {
o.headerContentRendered = true o.headerContentRendered = true
} else { } else {
o.logger.Debug("Ocean.Header: No actual header content lines to render.") o.logger.Debug("Ocean.Header: No actual header content lines to render.")
// If header is empty, table.go's renderHeader might still call Line() for the separator.
// o.headerContentRendered remains false if no content.
} }
// DO NOT draw the header separator line here. Let table.go's renderHeader or streamRenderHeader call o.Line().
} }
func (o *Ocean) Row(row []string, ctx tw.Formatting) { func (o *Ocean) Row(row []string, ctx tw.Formatting) {
@ -145,15 +136,6 @@ func (o *Ocean) Row(row []string, ctx tw.Formatting) {
if !o.widthsFinalized { if !o.widthsFinalized {
o.tryFinalizeWidths(ctx) o.tryFinalizeWidths(ctx)
} }
// Top border / header separator logic:
// If this is the very first output, table.go's batch renderHeader (or streamRenderHeader)
// should have already called Line() for top border and header separator.
// If Header() was called but rendered no content, table.go's renderHeader would still call Line() for the separator.
// If Header() was never called by table.go (e.g. streaming rows directly after Start()),
// then table.go's streamAppendRow needs to handle initial lines.
// Decision: Ocean.Row *only* renders row content.
if !o.widthsFinalized { if !o.widthsFinalized {
o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.") o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.")
return return
@ -171,11 +153,6 @@ func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) {
o.tryFinalizeWidths(ctx) o.tryFinalizeWidths(ctx)
o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).") o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).")
} }
// Separator line before footer:
// This should be handled by table.go's renderFooter or streamRenderFooter calling o.Line().
// Decision: Ocean.Footer *only* renders footer content.
if !o.widthsFinalized { if !o.widthsFinalized {
o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.") o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.")
return return
@ -194,24 +171,19 @@ func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) {
} else { } else {
o.logger.Debug("Ocean.Footer: No actual footer content lines to render.") o.logger.Debug("Ocean.Footer: No actual footer content lines to render.")
} }
// DO NOT draw the bottom border here. Let table.go's main Close or batch renderFooter call o.Line().
} }
func (o *Ocean) Line(ctx tw.Formatting) { func (o *Ocean) Line(ctx tw.Formatting) {
// This method is now called EXTERNALLY by table.go's batch or stream logic
// to draw all horizontal lines (top border, header sep, footer sep, bottom border).
if !o.widthsFinalized { if !o.widthsFinalized {
// If Line is called before widths are known (e.g. table.go's batch renderHeader trying to draw top border)
// we must try to finalize widths from this context.
o.tryFinalizeWidths(ctx) o.tryFinalizeWidths(ctx)
if !o.widthsFinalized { if !o.widthsFinalized {
o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.") o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.")
return return
} }
} }
// Ensure Line uses the consistent fixedWidths for drawing // Ensure Line uses the consistent fixedWidths for drawing
ctx.Row.Widths = o.fixedWidths ctx.Row.Widths = o.fixedWidths
o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len()) o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len())
jr := NewJunction(JunctionContext{ jr := NewJunction(JunctionContext{
@ -262,7 +234,7 @@ func (o *Ocean) Line(ctx tw.Formatting) {
if segmentChar == tw.Empty { if segmentChar == tw.Empty {
segmentChar = o.config.Symbols.Row() segmentChar = o.config.Symbols.Row()
} }
segmentDisplayWidth := tw.DisplayWidth(segmentChar) segmentDisplayWidth := twwidth.Width(segmentChar)
if segmentDisplayWidth <= 0 { if segmentDisplayWidth <= 0 {
segmentDisplayWidth = 1 segmentDisplayWidth = 1
} }
@ -296,12 +268,6 @@ func (o *Ocean) Line(ctx tw.Formatting) {
func (o *Ocean) Close() error { func (o *Ocean) Close() error {
o.logger.Debug("Ocean.Close() called.") o.logger.Debug("Ocean.Close() called.")
// The actual bottom border drawing is expected to be handled by table.go's
// batch render logic (renderFooter) or stream logic (streamRenderBottomBorder)
// by making an explicit call to o.Line() with the correct context.
// Ocean.Close() itself does not draw the bottom border to avoid duplication.
// Only reset state.
o.resetState() o.resetState()
return nil return nil
} }
@ -362,19 +328,16 @@ func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
idxInMergeSpan := colIdx + k idxInMergeSpan := colIdx + k
// Check if idxInMergeSpan is a defined column in fixedWidths // Check if idxInMergeSpan is a defined column in fixedWidths
foundInFixedWidths := false foundInFixedWidths := false
for _, sortedCIdx_inner := range sortedColIndices { if slices.Contains(sortedColIndices, idxInMergeSpan) {
if sortedCIdx_inner == idxInMergeSpan { currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan) foundInFixedWidths = true
foundInFixedWidths = true
break
}
} }
if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] { if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] {
o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan) o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan)
} }
if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() { if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
currentMergeTotalRenderWidth += tw.DisplayWidth(o.config.Symbols.Column()) currentMergeTotalRenderWidth += twwidth.Width(o.config.Symbols.Column())
} }
} }
actualCellWidthToRender = currentMergeTotalRenderWidth actualCellWidthToRender = currentMergeTotalRenderWidth
@ -428,7 +391,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t
return tw.Empty return tw.Empty
} }
contentDisplayWidth := tw.DisplayWidth(content) contentDisplayWidth := twwidth.Width(content)
padLeftChar := padding.Left padLeftChar := padding.Left
if padLeftChar == tw.Empty { if padLeftChar == tw.Empty {
@ -439,23 +402,17 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t
padRightChar = tw.Space padRightChar = tw.Space
} }
padLeftDisplayWidth := tw.DisplayWidth(padLeftChar) padLeftDisplayWidth := twwidth.Width(padLeftChar)
padRightDisplayWidth := tw.DisplayWidth(padRightChar) padRightDisplayWidth := twwidth.Width(padRightChar)
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth spaceForContentAndAlignment := max(cellVisualWidth-padLeftDisplayWidth-padRightDisplayWidth, 0)
if spaceForContentAndAlignment < 0 {
spaceForContentAndAlignment = 0
}
if contentDisplayWidth > spaceForContentAndAlignment { if contentDisplayWidth > spaceForContentAndAlignment {
content = tw.TruncateString(content, spaceForContentAndAlignment) content = twwidth.Truncate(content, spaceForContentAndAlignment)
contentDisplayWidth = tw.DisplayWidth(content) contentDisplayWidth = twwidth.Width(content)
} }
remainingSpace := spaceForContentAndAlignment - contentDisplayWidth remainingSpace := max(spaceForContentAndAlignment-contentDisplayWidth, 0)
if remainingSpace < 0 {
remainingSpace = 0
}
var PL, PR string var PL, PR string
switch align { switch align {
@ -477,7 +434,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t
sb.WriteString(PR) sb.WriteString(PR)
sb.WriteString(padRightChar) sb.WriteString(padRightChar)
currentFormattedWidth := tw.DisplayWidth(sb.String()) currentFormattedWidth := twwidth.Width(sb.String())
if currentFormattedWidth < cellVisualWidth { if currentFormattedWidth < cellVisualWidth {
if align == tw.AlignRight { if align == tw.AlignRight {
prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth) prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth)
@ -490,7 +447,7 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t
} else if currentFormattedWidth > cellVisualWidth { } else if currentFormattedWidth > cellVisualWidth {
tempStr := sb.String() tempStr := sb.String()
sb.Reset() sb.Reset()
sb.WriteString(tw.TruncateString(tempStr, cellVisualWidth)) sb.WriteString(twwidth.Truncate(tempStr, cellVisualWidth))
o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth) o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth)
} }
return sb.String() return sb.String()

View file

@ -2,11 +2,12 @@ package renderer
import ( import (
"fmt" "fmt"
"github.com/olekukonko/ll"
"html" "html"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@ -138,6 +139,7 @@ func NewSVG(configs ...SVGConfig) *SVG {
allVisualLineData: make([][][]string, 3), allVisualLineData: make([][][]string, 3),
allVisualLineCtx: make([][]tw.Formatting, 3), allVisualLineCtx: make([][]tw.Formatting, 3),
vMergeTrack: make(map[int]int), vMergeTrack: make(map[int]int),
logger: ll.New("svg"),
} }
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
r.allVisualLineData[i] = make([][]string, 0) r.allVisualLineData[i] = make([][]string, 0)
@ -511,10 +513,7 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
} }
s.dataRowCounter++ s.dataRowCounter++
} else { } else {
parentDataRowStripeIndex := s.dataRowCounter - 1 parentDataRowStripeIndex := max(s.dataRowCounter-1, 0)
if parentDataRowStripeIndex < 0 {
parentDataRowStripeIndex = 0
}
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 { if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
bgColor = s.config.RowAltBG bgColor = s.config.RowAltBG
} else { } else {
@ -622,9 +621,10 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
} }
} }
textX := currentX + s.config.Padding textX := currentX + s.config.Padding
if cellTextAnchor == "middle" { switch cellTextAnchor {
case "middle":
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0 textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
} else if cellTextAnchor == "end" { case "end":
textX = currentX + rectWidth - s.config.Padding textX = currentX + rectWidth - s.config.Padding
} }
textY := s.currentY + rectHeight/2.0 textY := s.currentY + rectHeight/2.0
@ -685,7 +685,7 @@ func (s *SVG) Start(w io.Writer) error {
func (s *SVG) debug(format string, a ...interface{}) { func (s *SVG) debug(format string, a ...interface{}) {
if s.config.Debug { if s.config.Debug {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
s.trace = append(s.trace, fmt.Sprintf("[SVG] %s", msg)) s.trace = append(s.trace, "[SVG] "+msg)
} }
} }

View file

@ -1,10 +1,11 @@
package tablewriter package tablewriter
import ( import (
"fmt"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/tw"
"math" "math"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
) )
// Close finalizes the table stream. // Close finalizes the table stream.
@ -89,11 +90,11 @@ func (t *Table) Start() error {
if !t.renderer.Config().Streaming { if !t.renderer.Config().Streaming {
// Check if the configured renderer actually supports streaming. // Check if the configured renderer actually supports streaming.
t.logger.Error("Configured renderer does not support streaming.") t.logger.Error("Configured renderer does not support streaming.")
return fmt.Errorf("renderer does not support streaming") return errors.Newf("renderer does not support streaming")
} }
//t.renderer.Start(t.writer) // t.renderer.Start(t.writer)
//t.renderer.Logger(t.logger) // t.renderer.Logger(t.logger)
if t.hasPrinted { if t.hasPrinted {
// Prevent calling Start() multiple times on the same stream instance. // Prevent calling Start() multiple times on the same stream instance.
@ -119,7 +120,7 @@ func (t *Table) Start() error {
t.streamWidths = t.config.Widths.PerColumn.Clone() t.streamWidths = t.config.Widths.PerColumn.Clone()
// Determine numCols from the highest index in PerColumn map // Determine numCols from the highest index in PerColumn map
maxColIdx := -1 maxColIdx := -1
t.streamWidths.Each(func(col int, width int) { t.streamWidths.Each(func(col, width int) {
if col > maxColIdx { if col > maxColIdx {
maxColIdx = col maxColIdx = col
} }
@ -207,7 +208,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row) rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row)
if err != nil { if err != nil {
t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err) t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err)
return fmt.Errorf("failed to convert row to strings: %w", err) return errors.Newf("failed to convert row to strings").Wrap(err)
} }
if len(rawCellsSlice) == 0 { if len(rawCellsSlice) == 0 {
@ -220,21 +221,34 @@ func (t *Table) streamAppendRow(row interface{}) error {
} }
if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil { if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil {
return err return errors.New("failed to establish stream column count/widths").Wrap(err)
} }
if t.streamNumCols > 0 && len(rawCellsSlice) != t.streamNumCols { // Now, check for column mismatch if a column count has been established.
t.logger.Warnf("streamAppendRow: Input row column count (%d) != stream column count (%d). Padding/Truncating.", len(rawCellsSlice), t.streamNumCols) if t.streamNumCols > 0 {
if len(rawCellsSlice) < t.streamNumCols { if len(rawCellsSlice) != t.streamNumCols {
paddedCells := make([]string, t.streamNumCols) if t.config.Stream.StrictColumns {
copy(paddedCells, rawCellsSlice) err := errors.Newf("input row column count (%d) does not match established stream column count (%d) and StrictColumns is enabled", len(rawCellsSlice), t.streamNumCols)
for i := len(rawCellsSlice); i < t.streamNumCols; i++ { t.logger.Error(err.Error())
paddedCells[i] = tw.Empty return err
}
// If not strict, retain the old lenient behavior (warn and pad/truncate)
t.logger.Warnf("streamAppendRow: Input row column count (%d) != stream column count (%d). Padding/Truncating (StrictColumns is false).", len(rawCellsSlice), t.streamNumCols)
if len(rawCellsSlice) < t.streamNumCols {
paddedCells := make([]string, t.streamNumCols)
copy(paddedCells, rawCellsSlice)
for i := len(rawCellsSlice); i < t.streamNumCols; i++ {
paddedCells[i] = tw.Empty
}
rawCellsSlice = paddedCells
} else {
rawCellsSlice = rawCellsSlice[:t.streamNumCols]
} }
rawCellsSlice = paddedCells
} else {
rawCellsSlice = rawCellsSlice[:t.streamNumCols]
} }
} else if len(rawCellsSlice) > 0 && t.config.Stream.StrictColumns {
err := errors.Newf("failed to establish stream column count from first data row (%d cells) and StrictColumns is enabled", len(rawCellsSlice))
t.logger.Error(err.Error())
return err
} }
if t.streamNumCols == 0 { if t.streamNumCols == 0 {
@ -318,7 +332,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.") t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.")
} else { } else {
details := "" details := ""
if !(shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) { if !shouldDrawHeaderRowSeparator && !shouldDrawRowRowSeparator {
details = "neither header/row nor row/row separator was flagged true" details = "neither header/row nor row/row separator was flagged true"
} else if t.lastRenderedPosition == tw.Position("separator") { } else if t.lastRenderedPosition == tw.Position("separator") {
details = "lastRenderedPosition is already 'separator'" details = "lastRenderedPosition is already 'separator'"
@ -462,7 +476,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
determinedNumCols := 0 determinedNumCols := 0
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 { if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
maxColIdx := -1 maxColIdx := -1
t.config.Widths.PerColumn.Each(func(col int, width int) { t.config.Widths.PerColumn.Each(func(col, width int) {
if col > maxColIdx { if col > maxColIdx {
maxColIdx = col maxColIdx = col
} }
@ -511,7 +525,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
ellipsisWidthBuffer := 0 ellipsisWidthBuffer := 0
if autoWrapForWidthCalc == tw.WrapTruncate { if autoWrapForWidthCalc == tw.WrapTruncate {
ellipsisWidthBuffer = tw.DisplayWidth(tw.CharEllipsis) ellipsisWidthBuffer = twwidth.Width(tw.CharEllipsis)
} }
varianceBuffer := 2 // Your suggested variance varianceBuffer := 2 // Your suggested variance
minTotalColWidth := tw.MinimumColumnWidth minTotalColWidth := tw.MinimumColumnWidth
@ -525,14 +539,14 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
if i < len(sampling) { if i < len(sampling) {
sampleContent = t.Trimmer(sampling[i]) sampleContent = t.Trimmer(sampling[i])
} }
sampleContentDisplayWidth := tw.DisplayWidth(sampleContent) sampleContentDisplayWidth := twwidth.Width(sampleContent)
colPad := paddingForWidthCalc.Global colPad := paddingForWidthCalc.Global
if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() { if i < len(paddingForWidthCalc.PerColumn) && paddingForWidthCalc.PerColumn[i].Paddable() {
colPad = paddingForWidthCalc.PerColumn[i] colPad = paddingForWidthCalc.PerColumn[i]
} }
currentPadLWidth := tw.DisplayWidth(colPad.Left) currentPadLWidth := twwidth.Width(colPad.Left)
currentPadRWidth := tw.DisplayWidth(colPad.Right) currentPadRWidth := twwidth.Width(colPad.Right)
currentTotalPaddingWidth := currentPadLWidth + currentPadRWidth currentTotalPaddingWidth := currentPadLWidth + currentPadRWidth
// Start with the target content width logic // Start with the target content width logic
@ -587,7 +601,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
if t.config.Widths.Global > 0 && t.streamNumCols > 0 { if t.config.Widths.Global > 0 && t.streamNumCols > 0 {
t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global) t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global)
currentTotalColumnWidthsSum := 0 currentTotalColumnWidthsSum := 0
t.streamWidths.Each(func(_ int, w int) { t.streamWidths.Each(func(_, w int) {
currentTotalColumnWidthsSum += w currentTotalColumnWidthsSum += w
}) })
@ -595,7 +609,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
if t.renderer != nil { if t.renderer != nil {
rendererConfig := t.renderer.Config() rendererConfig := t.renderer.Config()
if rendererConfig.Settings.Separators.BetweenColumns.Enabled() { if rendererConfig.Settings.Separators.BetweenColumns.Enabled() {
separatorWidth = tw.DisplayWidth(rendererConfig.Symbols.Column()) separatorWidth = twwidth.Width(rendererConfig.Symbols.Column())
} }
} else { } else {
separatorWidth = 1 // Default if renderer not available yet separatorWidth = 1 // Default if renderer not available yet
@ -652,7 +666,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
// Distribute remainingSpace (positive or negative) among non-zero width columns // Distribute remainingSpace (positive or negative) among non-zero width columns
if remainingSpace != 0 && t.streamNumCols > 0 { if remainingSpace != 0 && t.streamNumCols > 0 {
colsToAdjust := []int{} colsToAdjust := []int{}
t.streamWidths.Each(func(col int, w int) { t.streamWidths.Each(func(col, w int) {
if w > 0 { // Only consider columns that currently have width if w > 0 { // Only consider columns that currently have width
colsToAdjust = append(colsToAdjust, col) colsToAdjust = append(colsToAdjust, col)
} }
@ -676,7 +690,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
} }
// Final sanitization // Final sanitization
t.streamWidths.Each(func(col int, width int) { t.streamWidths.Each(func(col, width int) {
if width < 0 { if width < 0 {
t.streamWidths.Set(col, 0) t.streamWidths.Set(col, 0)
} }
@ -845,7 +859,7 @@ func (t *Table) streamRenderFooter(processedFooterLines [][]string) error {
// If this is the last line of the last content block (footer), and no bottom border will be drawn, // If this is the last line of the last content block (footer), and no bottom border will be drawn,
// its Location should be End. // its Location should be End.
isLastLineOfTableContent := (i == totalFooterLines-1) && isLastLineOfTableContent := (i == totalFooterLines-1) &&
!(cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled()) (!cfg.Borders.Bottom.Enabled() || !cfg.Settings.Lines.ShowBottom.Enabled())
if isLastLineOfTableContent { if isLastLineOfTableContent {
resp.location = tw.LocationEnd resp.location = tw.LocationEnd
t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.") t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.")

View file

@ -2,23 +2,27 @@ package tablewriter
import ( import (
"bytes" "bytes"
"fmt" "io"
"math"
"os"
"reflect"
"runtime"
"strings"
"sync"
"github.com/olekukonko/errors" "github.com/olekukonko/errors"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lh"
"github.com/olekukonko/tablewriter/pkg/twwarp" "github.com/olekukonko/tablewriter/pkg/twwarp"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
"io"
"math"
"reflect"
"strings"
"sync"
) )
// Table represents a table instance with content and rendering capabilities. // Table represents a table instance with content and rendering capabilities.
type Table struct { type Table struct {
writer io.Writer // Destination for table output writer io.Writer // Destination for table output
counters []tw.Counter // Counters for indices
rows [][][]string // Row data, supporting multi-line cells rows [][][]string // Row data, supporting multi-line cells
headers [][]string // Header content headers [][]string // Header content
footers [][]string // Footer content footers [][]string // Footer content
@ -177,65 +181,87 @@ func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modif
// This method always contributes to a single logical row in the table. // This method always contributes to a single logical row in the table.
// To add multiple distinct rows, call Append multiple times (once for each row's data) // To add multiple distinct rows, call Append multiple times (once for each row's data)
// or use the Bulk() method if providing a slice where each element is a row. // or use the Bulk() method if providing a slice where each element is a row.
func (t *Table) Append(rows ...interface{}) error { // rows is already []interface{} func (t *Table) Append(rows ...interface{}) error {
t.ensureInitialized() t.ensureInitialized()
if t.config.Stream.Enable && t.hasPrinted { if t.config.Stream.Enable && t.hasPrinted {
// Streaming logic remains unchanged, as AutoHeader is a batch-mode concept.
t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows)) t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows))
var rowItemForStream interface{} var rowItemForStream interface{}
if len(rows) == 1 { if len(rows) == 1 {
rowItemForStream = rows[0] rowItemForStream = rows[0]
} else { } else {
rowItemForStream = rows // Pass the slice of items if multiple args rowItemForStream = rows
} }
if err := t.streamAppendRow(rowItemForStream); err != nil { if err := t.streamAppendRow(rowItemForStream); err != nil {
t.logger.Errorf("Error rendering streaming row: %v", err) t.logger.Errorf("Error rendering streaming row: %v", err)
return fmt.Errorf("failed to stream append row: %w", err) return errors.Newf("failed to stream append row").Wrap(err)
} }
return nil return nil
} }
//Batch Mode Logic // Batch Mode Logic
t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows) t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows)
var cellsSource interface{} var cellsSource interface{}
if len(rows) == 1 { if len(rows) == 1 {
cellsSource = rows[0] cellsSource = rows[0]
t.logger.Debug("Append (Batch): Single argument provided. Treating it as the source for row cells.")
} else { } else {
cellsSource = rows // 'rows' is []interface{} containing all arguments cellsSource = rows
t.logger.Debug("Append (Batch): Multiple arguments provided. Treating them directly as cells for one row.") }
// Check if we should attempt to auto-generate headers from this append operation.
// Conditions: AutoHeader is on, no headers are set yet, and this is the first data row.
isFirstRow := len(t.rows) == 0
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow {
t.logger.Debug("Append: Triggering AutoHeader for the first row.")
headers := t.extractHeadersFromStruct(cellsSource)
if len(headers) > 0 {
// Set the extracted headers. The Header() method handles the rest.
t.Header(headers)
}
} }
if err := t.appendSingle(cellsSource); err != nil { // The rest of the function proceeds as before, converting the data to string lines.
lines, err := t.toStringLines(cellsSource, t.config.Row)
if err != nil {
t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err) t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err)
return err return err
} }
t.rows = append(t.rows, lines)
t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows)) t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows))
return nil return nil
} }
// Bulk adds multiple rows from a slice to the table (legacy method). // Bulk adds multiple rows from a slice to the table.
// Parameter rows must be a slice compatible with stringer or []string. // If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs,
// Returns an error if the input is invalid or appending fails. // automatically extracts/sets headers from the first struct.
func (t *Table) Bulk(rows interface{}) error { func (t *Table) Bulk(rows interface{}) error {
t.logger.Debug("Starting Bulk operation")
rv := reflect.ValueOf(rows) rv := reflect.ValueOf(rows)
if rv.Kind() != reflect.Slice { if rv.Kind() != reflect.Slice {
err := errors.Newf("Bulk expects a slice, got %T", rows) return errors.Newf("Bulk expects a slice, got %T", rows)
t.logger.Debugf("Bulk error: %v", err)
return err
} }
if rv.Len() == 0 {
return nil
}
// AutoHeader logic remains here, as it's a "Bulk" operation concept.
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 {
first := rv.Index(0).Interface()
// We can now correctly get headers from pointers or embedded structs
headers := t.extractHeadersFromStruct(first)
if len(headers) > 0 {
t.Header(headers)
}
}
// The rest of the logic is now just a loop over Append.
for i := 0; i < rv.Len(); i++ { for i := 0; i < rv.Len(); i++ {
row := rv.Index(i).Interface() row := rv.Index(i).Interface()
t.logger.Debugf("Processing bulk row %d: %v", i, row) if err := t.Append(row); err != nil { // Use Append
if err := t.appendSingle(row); err != nil {
t.logger.Debugf("Bulk append failed at index %d: %v", i, err)
return err return err
} }
} }
t.logger.Debugf("Bulk completed, processed %d rows", rv.Len())
return nil return nil
} }
@ -386,7 +412,6 @@ func (t *Table) Footer(elements ...any) {
// Parameter opts is a function that modifies the Table struct. // Parameter opts is a function that modifies the Table struct.
// Returns the Table instance for method chaining. // Returns the Table instance for method chaining.
func (t *Table) Options(opts ...Option) *Table { func (t *Table) Options(opts ...Option) *Table {
// add logger // add logger
if t.logger == nil { if t.logger == nil {
t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace)) t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace))
@ -399,7 +424,7 @@ func (t *Table) Options(opts ...Option) *Table {
// force debugging mode if set // force debugging mode if set
// This should be move away form WithDebug // This should be move away form WithDebug
if t.config.Debug == true { if t.config.Debug {
t.logger.Enable() t.logger.Enable()
t.logger.Resume() t.logger.Resume()
} else { } else {
@ -407,10 +432,14 @@ func (t *Table) Options(opts ...Option) *Table {
t.logger.Suspend() t.logger.Suspend()
} }
// help resolve from deprecation // Get additional system information for debugging
//if t.config.Stream.Enable { goVersion := runtime.Version()
// t.config.Widths = t.config.Stream.Widths goOS := runtime.GOOS
//} goArch := runtime.GOARCH
numCPU := runtime.NumCPU()
t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM"))
t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU)
// send logger to renderer // send logger to renderer
// this will overwrite the default logger // this will overwrite the default logger
@ -482,6 +511,28 @@ func (t *Table) Render() error {
return t.render() return t.render()
} }
// Lines returns the total number of lines rendered.
// This method is only effective if the WithLineCounter() option was used during
// table initialization and must be called *after* Render().
// It actively searches for the default tw.LineCounter among all active counters.
// It returns -1 if the line counter was not enabled.
func (t *Table) Lines() int {
for _, counter := range t.counters {
if lc, ok := counter.(*tw.LineCounter); ok {
return lc.Total()
}
}
// use -1 to indicate no line counter is attached
return -1
}
// Counters returns the slice of all active counter instances.
// This is useful when multiple counters are enabled.
// It must be called *after* Render().
func (t *Table) Counters() []tw.Counter {
return t.counters
}
// Trimmer trims whitespace from a string based on the Tables configuration. // Trimmer trims whitespace from a string based on the Tables configuration.
// It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior // It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior
// is enabled in t.config.Behavior, otherwise returning the string unchanged. This method // is enabled in t.config.Behavior, otherwise returning the string unchanged. This method
@ -635,7 +686,7 @@ func (t *Table) finalizeHierarchicalMergeBlock(ctx *renderContext, mctx *mergeCo
startState.Hierarchical.Present = true startState.Hierarchical.Present = true
startState.Hierarchical.Start = true startState.Hierarchical.Start = true
startState.Hierarchical.Span = finalSpan startState.Hierarchical.Span = finalSpan
startState.Hierarchical.End = (finalSpan == 1) startState.Hierarchical.End = finalSpan == 1
mctx.rowMerges[startRow][col] = startState mctx.rowMerges[startRow][col] = startState
} }
@ -756,7 +807,7 @@ func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
captionWrapWidth = t.caption.Width captionWrapWidth = t.caption.Width
t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth) t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth)
} else if actualTableWidth <= 4 { } else if actualTableWidth <= 4 {
captionWrapWidth = tw.DisplayWidth(t.caption.Text) captionWrapWidth = twwidth.Width(t.caption.Text)
t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth) t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth)
} else { } else {
captionWrapWidth = actualTableWidth captionWrapWidth = actualTableWidth
@ -879,8 +930,8 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
colPad = config.Padding.PerColumn[i] colPad = config.Padding.PerColumn[i]
} }
padLeftWidth := tw.DisplayWidth(colPad.Left) padLeftWidth := twwidth.Width(colPad.Left)
padRightWidth := tw.DisplayWidth(colPad.Right) padRightWidth := twwidth.Width(colPad.Right)
effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming) effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming)
@ -902,12 +953,12 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
} }
finalLinesForCell = append(finalLinesForCell, wrapped...) finalLinesForCell = append(finalLinesForCell, wrapped...)
case tw.WrapTruncate: case tw.WrapTruncate:
if tw.DisplayWidth(line) > effectiveContentMaxWidth { if twwidth.Width(line) > effectiveContentMaxWidth {
ellipsisWidth := tw.DisplayWidth(tw.CharEllipsis) ellipsisWidth := twwidth.Width(tw.CharEllipsis)
if effectiveContentMaxWidth >= ellipsisWidth { if effectiveContentMaxWidth >= ellipsisWidth {
finalLinesForCell = append(finalLinesForCell, tw.TruncateString(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis)) finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis))
} else { } else {
finalLinesForCell = append(finalLinesForCell, tw.TruncateString(line, effectiveContentMaxWidth, "")) finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth, ""))
} }
} else { } else {
finalLinesForCell = append(finalLinesForCell, line) finalLinesForCell = append(finalLinesForCell, line)
@ -915,12 +966,9 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
case tw.WrapBreak: case tw.WrapBreak:
wrapped := make([]string, 0) wrapped := make([]string, 0)
currentLine := line currentLine := line
breakCharWidth := tw.DisplayWidth(tw.CharBreak) breakCharWidth := twwidth.Width(tw.CharBreak)
for tw.DisplayWidth(currentLine) > effectiveContentMaxWidth { for twwidth.Width(currentLine) > effectiveContentMaxWidth {
targetWidth := effectiveContentMaxWidth - breakCharWidth targetWidth := max(effectiveContentMaxWidth-breakCharWidth, 0)
if targetWidth < 0 {
targetWidth = 0
}
breakPoint := tw.BreakPoint(currentLine, targetWidth) breakPoint := tw.BreakPoint(currentLine, targetWidth)
runes := []rune(currentLine) runes := []rune(currentLine)
if breakPoint <= 0 || breakPoint > len(runes) { if breakPoint <= 0 || breakPoint > len(runes) {
@ -929,7 +977,7 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
tempWidth := 0 tempWidth := 0
for charIdx, r := range runes { for charIdx, r := range runes {
runeStr := string(r) runeStr := string(r)
rw := tw.DisplayWidth(runeStr) rw := twwidth.Width(runeStr)
if tempWidth+rw > targetWidth && charIdx > 0 { if tempWidth+rw > targetWidth && charIdx > 0 {
break break
} }
@ -956,10 +1004,10 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
currentLine = string(runes[breakPoint:]) currentLine = string(runes[breakPoint:])
} }
} }
if tw.DisplayWidth(currentLine) > 0 { if twwidth.Width(currentLine) > 0 {
wrapped = append(wrapped, currentLine) wrapped = append(wrapped, currentLine)
} }
if len(wrapped) == 0 && tw.DisplayWidth(line) > 0 && len(finalLinesForCell) == 0 { if len(wrapped) == 0 && twwidth.Width(line) > 0 && len(finalLinesForCell) == 0 {
finalLinesForCell = append(finalLinesForCell, line) finalLinesForCell = append(finalLinesForCell, line)
} else { } else {
finalLinesForCell = append(finalLinesForCell, wrapped...) finalLinesForCell = append(finalLinesForCell, wrapped...)
@ -1334,23 +1382,40 @@ func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, posi
// No parameters are required. // No parameters are required.
// Returns an error if rendering fails in any section. // Returns an error if rendering fails in any section.
func (t *Table) render() error { func (t *Table) render() error {
t.ensureInitialized() t.ensureInitialized()
// Save the original writer and schedule its restoration upon function exit.
// This guarantees the table's writer is restored even if errors occur.
originalWriter := t.writer
defer func() {
t.writer = originalWriter
}()
// If a counter is active, wrap the writer in a MultiWriter.
if len(t.counters) > 0 {
// The slice must be of type io.Writer.
// Start it with the original destination writer.
allWriters := []io.Writer{originalWriter}
// Append each counter to the slice of writers.
for _, c := range t.counters {
allWriters = append(allWriters, c)
}
// Create a MultiWriter that broadcasts to the original writer AND all counters.
t.writer = io.MultiWriter(allWriters...)
}
if t.config.Stream.Enable { if t.config.Stream.Enable {
t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.") t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.")
return errors.New("render called in streaming mode; use Start/Append/Close") return errors.New("render called in streaming mode; use Start/Append/Close")
} }
// Calculate and cache numCols for THIS batch render pass // Calculate and cache the column count for this specific batch render pass.
t.batchRenderNumCols = t.maxColumns() // Calculate ONCE t.batchRenderNumCols = t.maxColumns()
t.isBatchRenderNumColsSet = true // Mark the cache as active for this render pass t.isBatchRenderNumColsSet = true
t.logger.Debugf("Render(): Set batchRenderNumCols to %d and isBatchRenderNumColsSet to true.", t.batchRenderNumCols)
defer func() { defer func() {
t.isBatchRenderNumColsSet = false t.isBatchRenderNumColsSet = false
// t.batchRenderNumCols = 0; // Optional: reset to 0, or leave as is.
// Since isBatchRenderNumColsSet is false, its value won't be used by getNumColsToUse.
t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols) t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols)
}() }()
@ -1359,9 +1424,10 @@ func (t *Table) render() error {
(t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight) (t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)
var tableStringBuffer *strings.Builder var tableStringBuffer *strings.Builder
targetWriter := t.writer targetWriter := t.writer // Can be the original writer or the MultiWriter.
originalWriter := t.writer // Save original writer for restoration if needed
// If a caption is present, the main table content must be rendered to an
// in-memory buffer first to calculate its final width.
if isTopOrBottomCaption { if isTopOrBottomCaption {
tableStringBuffer = &strings.Builder{} tableStringBuffer = &strings.Builder{}
targetWriter = tableStringBuffer targetWriter = tableStringBuffer
@ -1370,19 +1436,17 @@ func (t *Table) render() error {
t.logger.Debugf("No caption detected. Rendering table core directly to writer.") t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
} }
//Render Table Core // Point the table's writer to the target (either the final destination or the buffer).
t.writer = targetWriter t.writer = targetWriter
ctx, mctx, err := t.prepareContexts() ctx, mctx, err := t.prepareContexts()
if err != nil { if err != nil {
t.writer = originalWriter
t.logger.Errorf("prepareContexts failed: %v", err) t.logger.Errorf("prepareContexts failed: %v", err)
return fmt.Errorf("failed to prepare table contexts: %w", err) return errors.Newf("failed to prepare table contexts").Wrap(err)
} }
if err := ctx.renderer.Start(t.writer); err != nil { if err := ctx.renderer.Start(t.writer); err != nil {
t.writer = originalWriter
t.logger.Errorf("Renderer Start() error: %v", err) t.logger.Errorf("Renderer Start() error: %v", err)
return fmt.Errorf("renderer start failed: %w", err) return errors.Newf("renderer start failed").Wrap(err)
} }
renderError := false renderError := false
@ -1397,7 +1461,7 @@ func (t *Table) render() error {
if renderErr := renderFn(ctx, mctx); renderErr != nil { if renderErr := renderFn(ctx, mctx); renderErr != nil {
t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr) t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr)
if !renderError { if !renderError {
firstRenderErr = fmt.Errorf("failed to render %s section: %w", sectionName, renderErr) firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr)
} }
renderError = true renderError = true
break break
@ -1407,23 +1471,26 @@ func (t *Table) render() error {
if closeErr := ctx.renderer.Close(); closeErr != nil { if closeErr := ctx.renderer.Close(); closeErr != nil {
t.logger.Errorf("Renderer Close() error: %v", closeErr) t.logger.Errorf("Renderer Close() error: %v", closeErr)
if !renderError { if !renderError {
firstRenderErr = fmt.Errorf("renderer close failed: %w", closeErr) firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr)
} }
renderError = true renderError = true
} }
t.writer = originalWriter // Restore original writer // Restore the writer to the original for the caption-handling logic.
// This is necessary because the caption must be written to the final
// destination, not the temporary buffer used for the table body.
t.writer = originalWriter
if renderError { if renderError {
return firstRenderErr // Return error from core rendering if any return firstRenderErr
} }
//Caption Handling & Final Output --- // Caption Handling & Final Output
if isTopOrBottomCaption { if isTopOrBottomCaption {
renderedTableContent := tableStringBuffer.String() renderedTableContent := tableStringBuffer.String()
t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent)) t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent))
// Check if the buffer is empty AND borders are enabled // Handle edge case where table is empty but should have borders.
shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled()) shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled())
if len(renderedTableContent) == 0 && shouldHaveBorders { if len(renderedTableContent) == 0 && shouldHaveBorders {
var sb strings.Builder var sb strings.Builder
@ -1441,7 +1508,7 @@ func (t *Table) render() error {
actualTableWidth := 0 actualTableWidth := 0
trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t") trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t")
for _, line := range strings.Split(trimmedBuffer, "\n") { for _, line := range strings.Split(trimmedBuffer, "\n") {
w := tw.DisplayWidth(line) w := twwidth.Width(line)
if w > actualTableWidth { if w > actualTableWidth {
actualTableWidth = w actualTableWidth = w
} }
@ -1475,7 +1542,7 @@ func (t *Table) render() error {
t.hasPrinted = true t.hasPrinted = true
t.logger.Info("Render() completed.") t.logger.Info("Render() completed.")
return nil // Success return nil
} }
// renderFooter renders the table's footer section with borders and padding. // renderFooter renders the table's footer section with borders and padding.
@ -1649,7 +1716,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if hasTopPadding { if hasTopPadding {
hctx.rowIdx = 0 hctx.rowIdx = 0
hctx.lineIdx = -1 hctx.lineIdx = -1
if !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) { if !hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled() {
hctx.location = tw.LocationFirst hctx.location = tw.LocationFirst
} else { } else {
hctx.location = tw.LocationMiddle hctx.location = tw.LocationMiddle
@ -1671,7 +1738,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
hctx.line = padLine(line, ctx.numCols) hctx.line = padLine(line, ctx.numCols)
isFirstContentLine := i == 0 isFirstContentLine := i == 0
isLastContentLine := i == len(ctx.footerLines)-1 isLastContentLine := i == len(ctx.footerLines)-1
if isFirstContentLine && !hasTopPadding && !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) { if isFirstContentLine && !hasTopPadding && (!hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled()) {
hctx.location = tw.LocationFirst hctx.location = tw.LocationFirst
} else if isLastContentLine && !hasBottomPaddingConfig { } else if isLastContentLine && !hasBottomPaddingConfig {
hctx.location = tw.LocationEnd hctx.location = tw.LocationEnd
@ -1688,7 +1755,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if hasBottomPaddingConfig { if hasBottomPaddingConfig {
paddingLineContentForContext = make([]string, ctx.numCols) paddingLineContentForContext = make([]string, ctx.numCols)
formattedPaddingCells := make([]string, ctx.numCols) formattedPaddingCells := make([]string, ctx.numCols)
var representativePadChar string = " " representativePadChar := " "
ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings") ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings")
for j := 0; j < ctx.numCols; j++ { for j := 0; j < ctx.numCols; j++ {
colWd := ctx.widths[tw.Footer].Get(j) colWd := ctx.widths[tw.Footer].Get(j)
@ -1713,10 +1780,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if j == 0 || representativePadChar == " " { if j == 0 || representativePadChar == " " {
representativePadChar = padChar representativePadChar = padChar
} }
padWidth := tw.DisplayWidth(padChar) padWidth := max(twwidth.Width(padChar), 1)
if padWidth < 1 {
padWidth = 1
}
repeatCount := 0 repeatCount := 0
if colWd > 0 && padWidth > 0 { if colWd > 0 && padWidth > 0 {
repeatCount = colWd / padWidth repeatCount = colWd / padWidth
@ -1728,12 +1792,12 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
repeatCount = 0 repeatCount = 0
} }
rawPaddingContent := strings.Repeat(padChar, repeatCount) rawPaddingContent := strings.Repeat(padChar, repeatCount)
currentWd := tw.DisplayWidth(rawPaddingContent) currentWd := twwidth.Width(rawPaddingContent)
if currentWd < colWd { if currentWd < colWd {
rawPaddingContent += strings.Repeat(" ", colWd-currentWd) rawPaddingContent += strings.Repeat(" ", colWd-currentWd)
} }
if currentWd > colWd && colWd > 0 { if currentWd > colWd && colWd > 0 {
rawPaddingContent = tw.TruncateString(rawPaddingContent, colWd) rawPaddingContent = twwidth.Truncate(rawPaddingContent, colWd)
} }
if colWd == 0 { if colWd == 0 {
rawPaddingContent = "" rawPaddingContent = ""
@ -2111,19 +2175,21 @@ func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error {
hctx.lineIdx = j hctx.lineIdx = j
hctx.line = padLine(visualLineData, ctx.numCols) hctx.line = padLine(visualLineData, ctx.numCols)
if j > 0 { if t.config.Behavior.TrimLine.Enabled() {
visualLineHasActualContent := false if j > 0 {
for kCellIdx, cellContentInVisualLine := range hctx.line { visualLineHasActualContent := false
if t.Trimmer(cellContentInVisualLine) != "" { for kCellIdx, cellContentInVisualLine := range hctx.line {
visualLineHasActualContent = true if t.Trimmer(cellContentInVisualLine) != "" {
ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine) visualLineHasActualContent = true
break ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine)
break
}
} }
}
if !visualLineHasActualContent { if !visualLineHasActualContent {
ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line) ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line)
continue continue
}
} }
} }

View file

@ -12,7 +12,6 @@ type CellFormatting struct {
// Deprecated: kept for compatibility // Deprecated: kept for compatibility
// will be removed soon // will be removed soon
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center) Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
} }
// CellPadding defines padding settings for table cells. // CellPadding defines padding settings for table cells.

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