[bugfix] stricter property value counts (#4905)

- removes some duplicate logic in ./internal/ap and moves more of the functions to ./internal/ap/properties.go with standardized interfaces
- updates various areas of the codebase to rely on these GetOne___() functions
- pulls in latest gopkg for log.Formatted() function for nicer log formatting of variables in errors

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4905
This commit is contained in:
kim
2026-06-25 06:39:45 +02:00
committed by tobi
parent 24537b2f6f
commit 867ce73d07
26 changed files with 694 additions and 583 deletions
+3 -3
View File
@@ -11,16 +11,16 @@ replace github.com/gin-gonic/gin => codeberg.org/superseriousbusiness/gin v1.11.
require (
code.superseriousbusiness.org/activity v1.19.0
code.superseriousbusiness.org/exif-terminator v0.11.4
code.superseriousbusiness.org/gopkg v0.0.0-20260520114201-7a01c90db818
code.superseriousbusiness.org/gopkg v0.0.0-20260623123713-4ead213a2d22
code.superseriousbusiness.org/httpsig v1.5.0
code.superseriousbusiness.org/oauth2/v4 v4.5.4-0.20250812115401-3961e46a7384
codeberg.org/gruf/go-bytesize v1.0.4
codeberg.org/gruf/go-byteutil v1.3.0
codeberg.org/gruf/go-cache/v3 v3.6.2
codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf
codeberg.org/gruf/go-caller v0.0.0-20260615202027-bc5a6ff3cb31
codeberg.org/gruf/go-debug v1.3.0
codeberg.org/gruf/go-errors/v2 v2.3.4
codeberg.org/gruf/go-fastcopy v1.1.3
codeberg.org/gruf/go-fastcopy v1.2.1
codeberg.org/gruf/go-fastpath/v2 v2.0.1
codeberg.org/gruf/go-ffmpreg v0.6.20
codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf
Generated
+6 -6
View File
@@ -6,8 +6,8 @@ code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 h1:r9uq8StaSHYKJ
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0/go.mod h1:IK1OlR6APjVB3E9tuYGvf0qXMrwP+TrzcHS5rf4wffQ=
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 h1:I512jiIeXDC4//2BeSPrRM2ZS4wpBKUaPeTPxakMNGA=
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0/go.mod h1:SNHomXNW88o1pFfLHpD4KsCZLfcr4z5dm+xcX5SV10A=
code.superseriousbusiness.org/gopkg v0.0.0-20260520114201-7a01c90db818 h1:fvX4P0KrflQxJzfZ2S2rvyFNscBkoOdutBPi2Q4uMgs=
code.superseriousbusiness.org/gopkg v0.0.0-20260520114201-7a01c90db818/go.mod h1:PNtiv80R4b+z7HvnZbX69dAPmgRSDGts3SNcSC6Ti30=
code.superseriousbusiness.org/gopkg v0.0.0-20260623123713-4ead213a2d22 h1:Pz5KlaHuNUpuJzIcnqmvsnJ68MrB35gffvFs2NGJfZo=
code.superseriousbusiness.org/gopkg v0.0.0-20260623123713-4ead213a2d22/go.mod h1:f2KFg2HOM6YHEXR4wjnab9xPY2PmphjZaz7S6/+Y1Xc=
code.superseriousbusiness.org/httpsig v1.5.0 h1:jw/qc//yYWSoOYytTZXHvW7yh8kceCipNIBfUeXQghA=
code.superseriousbusiness.org/httpsig v1.5.0/go.mod h1:i2AKpj/WbA/o/UTvia9TAREzt0jP1AH3T1Uxjyhdzlw=
code.superseriousbusiness.org/oauth2/v4 v4.5.4-0.20250812115401-3961e46a7384 h1:eJzULGUyhHGk2DdQxX/jbH9FKZOyoIF90p3dzukCfLA=
@@ -18,14 +18,14 @@ codeberg.org/gruf/go-byteutil v1.3.0 h1:nRqJnCcRQ7xbfU6azw7zOzJrSMDIJHBqX6FL9vEM
codeberg.org/gruf/go-byteutil v1.3.0/go.mod h1:chgnZz1LUcfaObaIFglxF5MRYQkJGjQf4WwVz95ccCM=
codeberg.org/gruf/go-cache/v3 v3.6.2 h1:fQn7Dkj5gQpSNjlnoFeotXHKwbAh1PTH4qrD5BMm5ZA=
codeberg.org/gruf/go-cache/v3 v3.6.2/go.mod h1:yjmrOyda2K8B5sAfCpPPAAqI3oewf4mNQmvxygmP+g8=
codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf h1:Rzu7WLpscj2w1N+ClIHlJoTYf9SuqZrZ7E4f9T7jGdw=
codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf/go.mod h1:jEyYiqCzH1TaxfclSFYthE32oI0dsMnRS6EHqy6y0uo=
codeberg.org/gruf/go-caller v0.0.0-20260615202027-bc5a6ff3cb31 h1:yGDC2A1/WD2NuQ+dT9/7JNgPnTI/WAyEYX4y6ipjsA0=
codeberg.org/gruf/go-caller v0.0.0-20260615202027-bc5a6ff3cb31/go.mod h1:jEyYiqCzH1TaxfclSFYthE32oI0dsMnRS6EHqy6y0uo=
codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg=
codeberg.org/gruf/go-errors/v2 v2.3.4 h1:eC0UYpEx9y+aQgxv26JxjtYyN48OMil9JQ9cbHmw0ys=
codeberg.org/gruf/go-errors/v2 v2.3.4/go.mod h1:SpaIhBvi+hd4ohaS3UQJxA1OVmzApHBy4IufOn7OkD0=
codeberg.org/gruf/go-fastcopy v1.1.3 h1:Jo9VTQjI6KYimlw25PPc7YLA3Xm+XMQhaHwKnM7xD1g=
codeberg.org/gruf/go-fastcopy v1.1.3/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s=
codeberg.org/gruf/go-fastcopy v1.2.1 h1:RUpiim4H7CJKo+4aj0RIaS9EOYJ/hb1S5DGdH9wwsyg=
codeberg.org/gruf/go-fastcopy v1.2.1/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s=
codeberg.org/gruf/go-fastpath/v2 v2.0.1 h1:smCq3mpYiPibH4Na3SEN2rtr5oPmrY+cVRQ8nFCznJ4=
codeberg.org/gruf/go-fastpath/v2 v2.0.1/go.mod h1:ABH9P8jc/bUub/RW9yTI7i+Gm/Oqe6ZHcIGEumvAOKk=
codeberg.org/gruf/go-ffmpreg v0.6.20 h1:4Rc9yQLH1GTdMeiGPlMR1JUGNMJSjw6mRIxd+rxqDCo=
+26 -143
View File
@@ -91,9 +91,11 @@ func ExtractActivityObjectsAndInstruments(
rawJSON map[string]any,
) ([]TypeOrIRI, []any) {
switch typeName := activity.GetTypeName(); {
// Activity: has "object"
// and/or "instrument".
case isActivity(typeName):
// Gather object types and raw json.
objTypes, objJSON := extractObjectTypesAndJSON(activity, rawJSON)
@@ -312,26 +314,6 @@ func ExtractName(i WithName) string {
return ""
}
// ExtractInReplyToURI extracts the first inReplyTo URI
// property it can find from an interface. Will return
// nil if no valid URI can be found.
func ExtractInReplyToURI(i WithInReplyTo) *url.URL {
inReplyToProp := i.GetActivityStreamsInReplyTo()
if inReplyToProp == nil {
return nil
}
for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() {
iri, err := pub.ToId(iter)
if err == nil && iri != nil {
// Found one we can use.
return iri
}
}
return nil
}
// ExtractItemsURIs extracts each URI it can
// find for an item from the provided WithItems.
func ExtractItemsURIs(i WithItems) []*url.URL {
@@ -352,64 +334,6 @@ func ExtractItemsURIs(i WithItems) []*url.URL {
return uris
}
// ExtractToURIs returns a slice of URIs
// that the given WithTo addresses as To.
func ExtractToURIs(i WithTo) []*url.URL {
toProp := i.GetActivityStreamsTo()
if toProp == nil {
return nil
}
uris := make([]*url.URL, 0, toProp.Len())
for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
uri, err := pub.ToId(iter)
if err == nil {
// Found one we can use.
uris = append(uris, uri)
}
}
return uris
}
// ExtractCcURIs returns a slice of URIs
// that the given WithCC addresses as Cc.
func ExtractCcURIs(i WithCc) []*url.URL {
ccProp := i.GetActivityStreamsCc()
if ccProp == nil {
return nil
}
urls := make([]*url.URL, 0, ccProp.Len())
for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
uri, err := pub.ToId(iter)
if err == nil {
// Found one we can use.
urls = append(urls, uri)
}
}
return urls
}
// ExtractAttributedToURI returns the first URI it can find in the
// given WithAttributedTo, or an error if no URI can be found.
func ExtractAttributedToURI(i WithAttributedTo) (*url.URL, error) {
attributedToProp := i.GetActivityStreamsAttributedTo()
if attributedToProp == nil {
return nil, gtserror.New("attributedToProp was nil")
}
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
id, err := pub.ToId(iter)
if err == nil {
return id, nil
}
}
return nil, gtserror.New("couldn't find iri for attributed to")
}
// ExtractIconURI extracts the first URI it can find from
// the given WithIcon which links to a supported image file.
// Input will look something like this:
@@ -691,21 +615,20 @@ func ExtractPubKeyFromActor(i WithPublicKey) (
return nil, nil, nil, gtserror.New("public key property was nil")
}
// Take the first public key we can find.
for iter := pubKeyProp.Begin(); iter != pubKeyProp.End(); iter = iter.Next() {
if !iter.IsW3IDSecurityV1PublicKey() {
continue
}
pkey := iter.Get()
if pkey == nil {
continue
}
return ExtractPubKeyFromKey(pkey)
if l := pubKeyProp.Len(); l != 1 {
return nil, nil, nil, gtserror.Newf("unexpected public key property length: %d", l)
}
return nil, nil, nil, gtserror.New("couldn't find valid public key")
// Start property iterator.
iter := pubKeyProp.Begin()
// Ensure this is a expected valid key.
if !iter.IsW3IDSecurityV1PublicKey() {
return nil, nil, nil, gtserror.New("couldn't find valid public key")
}
// Extract RSA details from key type.
return ExtractPubKeyFromKey(iter.Get())
}
// ExtractPubKeyFromActor extracts the public key, public key ID, and public
@@ -718,32 +641,32 @@ func ExtractPubKeyFromKey(pkey vocab.W3IDSecurityV1PublicKey) (
) {
pubKeyID, err := pub.GetId(pkey)
if err != nil {
return nil, nil, nil, errors.New("no id set on public key")
return nil, nil, nil, gtserror.New("no id set on public key")
}
pubKeyOwnerProp := pkey.GetW3IDSecurityV1Owner()
if pubKeyOwnerProp == nil {
return nil, nil, nil, errors.New("nil pubKeyOwnerProp")
return nil, nil, nil, gtserror.New("nil pubKeyOwnerProp")
}
pubKeyOwner := pubKeyOwnerProp.GetIRI()
if pubKeyOwner == nil {
return nil, nil, nil, errors.New("nil iri on pubKeyOwnerProp")
return nil, nil, nil, gtserror.New("nil iri on pubKeyOwnerProp")
}
pubKeyPemProp := pkey.GetW3IDSecurityV1PublicKeyPem()
if pubKeyPemProp == nil {
return nil, nil, nil, errors.New("nil pubKeyPemProp")
return nil, nil, nil, gtserror.New("nil pubKeyPemProp")
}
pkeyPem := pubKeyPemProp.Get()
if pkeyPem == "" {
return nil, nil, nil, errors.New("empty pubKeyPemProp")
return nil, nil, nil, gtserror.New("empty pubKeyPemProp")
}
block, _ := pem.Decode([]byte(pkeyPem))
if block == nil {
return nil, nil, nil, errors.New("nil pubKeyPem")
return nil, nil, nil, gtserror.New("nil pubKeyPem")
}
var p crypto.PublicKey
@@ -753,20 +676,19 @@ func ExtractPubKeyFromKey(pkey vocab.W3IDSecurityV1PublicKey) (
case "RSA PUBLIC KEY":
p, err = x509.ParsePKCS1PublicKey(block.Bytes)
default:
err = fmt.Errorf("unknown block type: %q", block.Type)
err = gtserror.Newf("unknown block type: %q", block.Type)
}
if err != nil {
err = fmt.Errorf("could not parse public key from block bytes: %w", err)
return nil, nil, nil, err
return nil, nil, nil, gtserror.Newf("could not parse public key from block bytes: %w", err)
}
if p == nil {
return nil, nil, nil, fmt.Errorf("returned public key was empty")
return nil, nil, nil, gtserror.New("returned public key was empty")
}
pubKey, ok := p.(*rsa.PublicKey)
if !ok {
return nil, nil, nil, fmt.Errorf("could not type pubKey to *rsa.PublicKey")
return nil, nil, nil, gtserror.New("could not type pubKey to *rsa.PublicKey")
}
return pubKey, pubKeyID, pubKeyOwner, nil
@@ -1209,45 +1131,6 @@ func ExtractActorURI(withActor WithActor) (*url.URL, error) {
return nil, gtserror.New("no iri found for actor prop")
}
// ExtractObjectURI extracts the first Object URI
// it can find from a WithObject interface.
func ExtractObjectURI(withObject WithObject) (*url.URL, error) {
objectProp := withObject.GetActivityStreamsObject()
if objectProp == nil {
return nil, gtserror.New("object property was nil")
}
for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
id, err := pub.ToId(iter)
if err == nil {
// Found one we can use.
return id, nil
}
}
return nil, gtserror.New("no iri found for object prop")
}
// ExtractObjectURIs extracts the URLs of each Object
// it can find from a WithObject interface.
func ExtractObjectURIs(withObject WithObject) ([]*url.URL, error) {
objectProp := withObject.GetActivityStreamsObject()
if objectProp == nil {
return nil, gtserror.New("object property was nil")
}
urls := make([]*url.URL, 0, objectProp.Len())
for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
id, err := pub.ToId(iter)
if err == nil {
// Found one we can use.
urls = append(urls, id)
}
}
return urls, nil
}
// ExtractVisibility extracts the gtsmodel.Visibility
// of a given addressable with a To and CC property.
//
@@ -1258,8 +1141,8 @@ func ExtractObjectURIs(withObject WithObject) ([]*url.URL, error) {
// eg., `https://example.org/users/whoever/followers`.
func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmodel.Visibility, error) {
var (
to = ExtractToURIs(addressable)
cc = ExtractCcURIs(addressable)
to = GetTo(addressable)
cc = GetCc(addressable)
)
if len(to) == 0 && len(cc) == 0 {
+96 -13
View File
@@ -24,6 +24,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gopkg/log"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
@@ -84,7 +85,7 @@ func SetJSONLDIdStr(with WithJSONLDId, id string) error {
// GetTo returns the IRIs contained in the To property of 'with'. Panics on entries with missing ID.
func GetTo(with WithTo) []*url.URL {
toProp := with.GetActivityStreamsTo()
return getIRIs[vocab.ActivityStreamsToPropertyIterator](toProp)
return getIRIs(toProp)
}
// AppendTo appends the given IRIs to the To property of 'with'.
@@ -102,7 +103,7 @@ func AppendTo(with WithTo, to ...*url.URL) {
// GetCc returns the IRIs contained in the Cc property of 'with'. Panics on entries with missing ID.
func GetCc(with WithCc) []*url.URL {
ccProp := with.GetActivityStreamsCc()
return extractIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp)
return extractIRIs(ccProp)
}
// AppendCc appends the given IRIs to the Cc property of 'with'.
@@ -120,7 +121,7 @@ func AppendCc(with WithCc, cc ...*url.URL) {
// GetBcc returns the IRIs contained in the Bcc property of 'with'. Panics on entries with missing ID.
func GetBcc(with WithBcc) []*url.URL {
bccProp := with.GetActivityStreamsBcc()
return extractIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp)
return extractIRIs(bccProp)
}
// AppendBcc appends the given IRIs to the Bcc property of 'with'.
@@ -190,10 +191,24 @@ func AppendURL(with WithURL, url ...*url.URL) {
}
}
// GetOneActorIRI extracts a single `actor` property IRI from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 actor value.
func GetOneActorIRI(with WithActor) (*url.URL, error) {
actors := GetActorIRIs(with)
if len(actors) != 1 {
return nil, gtserror.NewfAt(3, "expected single actor, received: %v", log.Formatted(actors))
}
return actors[0], nil
}
// If > 1 is accepted, but only first wanted, use:
//
// func GetFirstActorIRI(...) (*url.URL, error) {}
// GetActorIRIs returns the IRIs contained in the Actor property of 'with'.
func GetActorIRIs(with WithActor) []*url.URL {
actorProp := with.GetActivityStreamsActor()
return extractIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp)
return extractIRIs(actorProp)
}
// AppendActorIRIs appends the given IRIs to the Actor property of 'with'.
@@ -208,10 +223,24 @@ func AppendActorIRIs(with WithActor, actor ...*url.URL) {
}, actor...)
}
// GetOneObjectIRI extracts a single `object` property IRI from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 object value.
func GetOneObjectIRI(with WithObject) (*url.URL, error) {
objects := GetObjectIRIs(with)
if len(objects) != 1 {
return nil, gtserror.NewfAt(3, "expected single object, received: %v", log.Formatted(objects))
}
return objects[0], nil
}
// If > 1 is accepted, but only first wanted, use:
//
// func GetFirstObjectIRI(...) (*url.URL, error) {}
// GetObjectIRIs returns the IRIs contained in the Object property of 'with'.
func GetObjectIRIs(with WithObject) []*url.URL {
objectProp := with.GetActivityStreamsObject()
return extractIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp)
return extractIRIs(objectProp)
}
// AppendObjectIRIs appends the given IRIs to the Object property of 'with'.
@@ -238,6 +267,16 @@ func AppendInstrumentIRIs(with WithInstrument, instrument ...*url.URL) {
}, instrument...)
}
// GetOneResultIRI extracts a single `result` property IRI from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 result value.
func GetOneResultIRI(with WithResult) (*url.URL, error) {
results := GetResultIRIs(with)
if len(results) != 1 {
return nil, gtserror.NewfAt(3, "expected single result, received: %v", log.Formatted(results))
}
return results[0], nil
}
// GetResultIRIs returns the IRIs contained in the `result` property of 'with'.
func GetResultIRIs(with WithResult) []*url.URL {
resultProp := with.GetActivityStreamsResult()
@@ -256,13 +295,23 @@ func AppendResultIRIs(with WithResult, result ...*url.URL) {
}, result...)
}
// GetTargetIRIs returns the IRIs contained in the Target property of 'with'.
func GetTargetIRIs(with WithTarget) []*url.URL {
targetProp := with.GetActivityStreamsTarget()
return extractIRIs[vocab.ActivityStreamsTargetPropertyIterator](targetProp)
// GetOneTargetIRI extracts a single `target` IRI property from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 target value.
func GetOneTargetIRI(with WithTarget) (*url.URL, error) {
targets := GetTargetIRIs(with)
if len(targets) != 1 {
return nil, gtserror.NewfAt(3, "expected single target, received: %v", log.Formatted(targets))
}
return targets[0], nil
}
// AppendTargetIRIs appends the given IRIs to the Target property of 'with'.
// GetTargetIRIs returns the IRIs contained in the `target` property of 'with'.
func GetTargetIRIs(with WithTarget) []*url.URL {
targetProp := with.GetActivityStreamsTarget()
return extractIRIs(targetProp)
}
// AppendTargetIRIs appends the given IRIs to the `target` property of 'with'.
func AppendTargetIRIs(with WithTarget, target ...*url.URL) {
appendIRIs(func() Property[vocab.ActivityStreamsTargetPropertyIterator] {
targetProp := with.GetActivityStreamsTarget()
@@ -274,10 +323,24 @@ func AppendTargetIRIs(with WithTarget, target ...*url.URL) {
}, target...)
}
// GetOneAttributedTo extracts a single attributedTo property from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 attributedTo value.
func GetOneAttributedTo(with WithAttributedTo) (*url.URL, error) {
attribTo := GetAttributedTo(with)
if len(attribTo) != 1 {
return nil, gtserror.NewfAt(3, "expected single attributedTo, received: %v", log.Formatted(attribTo))
}
return attribTo[0], nil
}
// If > 1 is accepted, but only first wanted, use:
//
// func GetFirstAttributedTo(...) (*url.URL, error) {}
// GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'.
func GetAttributedTo(with WithAttributedTo) []*url.URL {
attribProp := with.GetActivityStreamsAttributedTo()
return extractIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp)
return extractIRIs(attribProp)
}
// AppendAttributedTo appends the given IRIs to the AttributedTo property of 'with'.
@@ -292,6 +355,16 @@ func AppendAttributedTo(with WithAttributedTo, attribTo ...*url.URL) {
}, attribTo...)
}
// GetOneInteractingObject extracts a single interactingObject property from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 interactingObject value.
func GetOneInteractingObject(with WithInteractingObject) (*url.URL, error) {
object := GetInteractingObject(with)
if len(object) != 1 {
return nil, gtserror.NewfAt(3, "expected single interactingObject, received: %v", log.Formatted(object))
}
return object[0], nil
}
// GetInteractingObject returns IRIs contained in the interactingObject property of 'with'.
func GetInteractingObject(with WithInteractingObject) []*url.URL {
intObjProp := with.GetGoToSocialInteractingObject()
@@ -310,6 +383,16 @@ func AppendInteractingObject(with WithInteractingObject, interactingObject ...*u
}, interactingObject...)
}
// GetOneInteractionTarget extracts a single interactingTarget property from 'with', else returns error.
// Note: it is an error for the passed type to return any count other than 1 interactionTarget value.
func GetOneInteractionTarget(with WithInteractionTarget) (*url.URL, error) {
target := GetInteractionTarget(with)
if len(target) != 1 {
return nil, gtserror.NewfAt(3, "expected single interactionTarget, received: %v", log.Formatted(target))
}
return target[0], nil
}
// GetInteractionTarget returns IRIs contained in the interactionTarget property of 'with'.
func GetInteractionTarget(with WithInteractionTarget) []*url.URL {
intTargetProp := with.GetGoToSocialInteractionTarget()
@@ -331,7 +414,7 @@ func AppendInteractionTarget(with WithInteractionTarget, interactionTarget ...*u
// GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'.
func GetInReplyTo(with WithInReplyTo) []*url.URL {
replyProp := with.GetActivityStreamsInReplyTo()
return extractIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp)
return extractIRIs(replyProp)
}
// AppendInReplyTo appends the given IRIs to the InReplyTo property of 'with'.
@@ -463,7 +546,7 @@ func SetMovedTo(with WithMovedTo, movedTo *url.URL) {
// GetAlsoKnownAs returns the IRI contained in the alsoKnownAs property of 'with'.
func GetAlsoKnownAs(with WithAlsoKnownAs) []*url.URL {
alsoKnownAsProp := with.GetActivityStreamsAlsoKnownAs()
return getIRIs[vocab.ActivityStreamsAlsoKnownAsPropertyIterator](alsoKnownAsProp)
return getIRIs(alsoKnownAsProp)
}
// SetAlsoKnownAs sets the given IRIs on the alsoKnownAs property of 'with'.
+9 -10
View File
@@ -254,7 +254,7 @@ func (d *Dereferencer) getStatusByURI(
// Search the database
// for existing status.
uriStr := uri.String()
status, err = d.getStatusDBOnly(ctx, uriStr)
status, err = d.getStatusFromDB(ctx, uriStr)
if err != nil {
return nil, nil, false, err
}
@@ -401,7 +401,7 @@ func (d *Dereferencer) enrichAndStoreStatusSafely(
// DATA RACE! We likely lost out to another goroutine
// in a call to db.Put(Status). Look again in DB by URI.
latest, err = d.getStatusDBOnly(ctx, status.URI)
latest, err = d.getStatusFromDB(ctx, status.URI)
if err != nil {
err = gtserror.Newf("error getting status %s from database after race: %w", uriStr, err)
}
@@ -450,12 +450,11 @@ func (d *Dereferencer) enrichAndStoreStatus(
return nil, nil, isNew, gtserror.Newf("couldn't create transport: %w", err)
}
// Dereference statusable from remote,
// checking if we already had this
// status stored under a different URI
// (ie., the final URI after redirects).
var alreadyStatus *gtsmodel.Status
statusable, alreadyStatus, err = d.retrieveStatusable(ctx, tsport, uri)
var existing *gtsmodel.Status
// Dereference statusable from remote, checking if we already had this
// status stored under a different URI (ie., the final URI after redirects).
statusable, existing, uri, err = d.retrieveStatusable(ctx, tsport, uri)
if err != nil {
return nil, nil, isNew, err
}
@@ -466,8 +465,8 @@ func (d *Dereferencer) enrichAndStoreStatus(
//
// Continue with this status and mark it as not
// new so we don't try to store it again below.
if alreadyStatus != nil {
status = alreadyStatus
if existing != nil {
status = existing
isNew = false
}
}
@@ -22,7 +22,7 @@ import (
"errors"
"net/url"
"code.superseriousbusiness.org/gopkg/xslices"
"code.superseriousbusiness.org/gopkg/log"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
@@ -35,10 +35,11 @@ import (
// getStatusDBOnly checks in the database for
// status with the given URI (or URL), without
// doing any external dereferencing.
func (d *Dereferencer) getStatusDBOnly(
func (d *Dereferencer) getStatusFromDB(
ctx context.Context,
uriStr string,
) (*gtsmodel.Status, error) {
// For both queries request a barebones
// object, as it will be later populated
// in the enrichAndStoreSafely() function.
@@ -77,23 +78,27 @@ func (d *Dereferencer) getStatusDBOnly(
//
// Will return malformed if the final redirected URI is not
// either the AP ID/URI or the URL of the dereffed statusable.
//
// The final returned URI is the dereferenced
// ActivityPub status object's JSON-LD ID.
func (d *Dereferencer) retrieveStatusable(
ctx context.Context,
tsport transport.Transport,
uri *url.URL,
) (
statusable ap.Statusable,
alreadyStatus *gtsmodel.Status,
existing *gtsmodel.Status,
statusURI *url.URL,
err error,
) {
// Save this for later comparison.
// Save for later comparison.
initialURIStr := uri.String()
// Dereference latest version of the status.
// Dereference latest version of status.
rsp, err := tsport.Dereference(ctx, uri)
if err != nil {
err := gtserror.Newf("error dereferencing %s: %w", uri, err)
return nil, nil, gtserror.SetUnretrievable(err)
err := gtserror.Newf("error dereferencing %s: %w", initialURIStr, err)
return nil, nil, nil, gtserror.SetUnretrievable(err)
}
// Attempt to resolve ActivityPub status from response.
@@ -105,8 +110,9 @@ func (d *Dereferencer) retrieveStatusable(
if err != nil {
// ResolveStatusable will set gtserror.WrongType
// on the returned error, so we don't need to do it here.
err := gtserror.Newf("error resolving statusable %s: %w", uri, err)
return nil, nil, err
err := gtserror.Newf("error resolving statusable %s: %w",
initialURIStr, err)
return nil, nil, nil, err
}
// Check whether input URI and final returned URI
@@ -114,72 +120,55 @@ func (d *Dereferencer) retrieveStatusable(
//
// NOTE: this URI check + database call is performed
// AFTER reading and closing body, for performance.
var (
finalURI = rsp.Request.URL
finalURIStr = rsp.Request.URL.String()
redirected = finalURIStr != initialURIStr
)
finalURI := rsp.Request.URL
finalURIStr := finalURI.String()
redirected := finalURIStr != initialURIStr
if redirected {
// Update passed-in URI
// for benefit of the caller.
uri = finalURI
// Check whether we have this status
// stored under *final* URI and return
// it to the caller if so.
var err error
alreadyStatus, err = d.getStatusDBOnly(ctx, finalURIStr)
// Check whether we have this status stored under
// *final* determined URI, preferring this for return.
existing, err = d.getStatusFromDB(ctx, finalURIStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting status after redirects: %w", err)
return nil, nil, err
return nil, nil, nil, err
}
}
// Extract the json-ld ID, i.e. the
// actual ActivityPub URI ID of status.
jsonldID := ap.GetJSONLDId(statusable)
// Ensure the final URI we fetched the status
// from matches either (one of) the URL(s) or
// the ID/URI of the dereferenced statusable.
okURIs := append(
ap.GetURL(statusable), // status URL(s)
ap.GetJSONLDId(statusable), // status URI
)
matches, err := util.URIMatches(finalURI, okURIs...)
uris := append(ap.GetURL(statusable), jsonldID)
matches, err := util.URIMatches(finalURI, uris...)
if err != nil {
err := gtserror.Newf("error checking final dereferenced status uri %s: %w", finalURIStr, err)
return nil, nil, err
err := gtserror.Newf("error checking uri matches %s: %w", finalURIStr, err)
return nil, nil, nil, gtserror.SetMalformed(err)
}
if !matches {
// There's not a match, so the remote is doing
// something weird. Gather URI strings we would
// have accepted into nice slice for logging.
okURIStrs := xslices.Gather(
nil,
okURIs,
func(u *url.URL) string {
return u.String()
},
)
// Construct error to give a bit more information
// in case there were one or more redirects.
var err error
// For error, include
// redirect for context.
uristr := finalURIStr
if redirected {
err = gtserror.Newf(
"final http URI %s, after redirect(s) from initial URI %s, does not match dereferenced statusable id or url(s) %+v",
finalURIStr, initialURIStr, okURIStrs,
)
} else {
err = gtserror.Newf(
"http URI %s does not match dereferenced statusable id or url(s) %+v",
initialURIStr, okURIStrs,
)
uristr += " (redirected from " + initialURIStr + ")"
}
// Set malformed on the returned error.
return nil, nil, gtserror.SetMalformed(err)
// No URI match, remote is doing something weird. Return malformed error type.
err := gtserror.Newf("fetch uri %s does not match known status uri(s): %v",
uristr, log.Formatted(uris))
return nil, nil, nil, gtserror.SetMalformed(err)
}
// For the final returned URI we set
// status' canonical JSON-LD URI, as
// checks against its URI won't work
// if we return an alternative URL.
statusURI = jsonldID
return
}
@@ -189,13 +178,21 @@ func (d *Dereferencer) retrieveStatusable(
func (d *Dereferencer) convertStatusable(
ctx context.Context,
requestUser string,
uri *url.URL,
statusURI *url.URL,
statusable ap.Statusable,
) (*gtsmodel.Status, error) {
// Get the attributed-to ID/URI in order to fetch account.
attributedTo, err := ap.ExtractAttributedToURI(statusable)
// Get attributedTo URI from statusable to fetch account.
attributedTo, err := ap.GetOneAttributedTo(statusable)
if err != nil {
return nil, gtserror.New("attributedTo was empty")
return nil, gtserror.SetMalformed(err)
}
// The status author, and the status
// JSON-LD ID must have the same host.
if attributedTo.Host != statusURI.Host {
err := gtserror.Newf("id and attributedTo hostnames differ: id=%s attributedTo=%s", attributedTo.Host, statusURI.Host)
return nil, gtserror.SetMalformed(err)
}
// Ensure we have the author account of the status dereferenced
@@ -204,13 +201,13 @@ func (d *Dereferencer) convertStatusable(
// Note that we specifically DO NOT wrap the error, instead collapsing it as string.
// Errors fetching an account do not necessarily relate to dereferencing the status.
return nil, gtserror.Newf("failed to dereference status author %s: %v", uri, err)
return nil, gtserror.Newf("failed to dereference status author %s: %v", statusURI, err)
}
// Convert AP model to our GTS model.
// Convert ActivityPub model to our internal GTS model.
status, err := d.converter.ASStatusToStatus(ctx, statusable)
if err != nil {
return nil, gtserror.Newf("error converting statusable to gts model for status %s: %w", uri, err)
return nil, gtserror.Newf("error converting statusable to gts model for status %s: %w", statusURI, err)
}
// Ensure final status isn't attempting
@@ -724,17 +724,13 @@ func (d *Dereferencer) isValidAuthURI(
// as the ID of the Accept we were returned.
switch {
case rspURL.Host != authIRI.Host:
l.Errorf(
"final deref host %s did not match authIRI host",
rspURL.Host,
)
l.Errorf("final deref host %s did not match authIRI host",
rspURL.Host)
return false, nil
case authIDStr != rspURLStr:
l.Errorf(
"final deref uri %s did not match returned ID %s",
rspURLStr, authIDStr,
)
l.Errorf("final deref uri %s did not match returned ID %s",
rspURLStr, authIDStr)
return false, nil
}
}
@@ -744,27 +740,33 @@ func (d *Dereferencer) isValidAuthURI(
// First try to parse type as Authorization stamp.
if authable, ok := ap.ToAuthorizationable(t); ok {
return isValidAuthorization(
ctx,
if err := isValidAuthorization(
interactionType,
authable,
authID,
expectActorURIStr, // actor
expectObjectURIStr, // object
expectTargetURIStr, // target
)
); err != nil {
log.Warn(ctx, err)
return false, nil
}
return true, nil
}
// Fall back to parsing as a simple Accept.
if acceptable, ok := ap.ToAcceptable(t); ok {
return isValidAcceptable(
ctx,
if err := isValidAcceptable(
acceptable,
authID,
expectActorURIStr, // actor
expectObjectURIStr, // object
expectTargetURIStr, // target
)
); err != nil {
log.Warn(ctx, err)
return false, nil
}
return true, nil
}
// Type wasn't something we
@@ -777,94 +779,85 @@ func (d *Dereferencer) isValidAuthURI(
}
func isValidAcceptable(
ctx context.Context,
acceptable ap.Acceptable,
acceptID *url.URL,
expectActorURIStr string, // actor Eg., "https://example.org/users/someone"
expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E"
) (bool, error) {
l := log.
WithContext(ctx).
WithField("accept", acceptID.String())
) error {
// Extract the actor IRI and string from Accept.
actorIRIs := ap.GetActorIRIs(acceptable)
actorIRI, actorIRIStr := extractIRI(actorIRIs)
switch {
case actorIRIStr == "":
l.Error("Accept missing actor IRI")
return false, nil
// Extract the actor IRI from acceptable type.
actorIRI, err := ap.GetOneActorIRI(acceptable)
if err != nil {
return err
}
// Serialize actor ID just once.
actorIRIStr := actorIRI.String()
// Ensure the Accept Actor is on
// the instance hosting the Accept.
case actorIRI.Host != acceptID.Host:
l.Errorf(
"actor %s not on the same host as Accept",
actorIRIStr,
)
return false, nil
if actorIRI.Host != acceptID.Host {
return gtserror.Newf("actor %s not on the same host as Accept",
actorIRIStr)
}
// Ensure the Accept Actor is who we expect
// it to be, and not someone else trying to
// do an Accept for an interaction with a
// statusable they don't own.
case actorIRIStr != expectActorURIStr:
l.Errorf(
"actor %s was not the same as expected actor %s",
actorIRIStr, expectActorURIStr,
)
return false, nil
if actorIRIStr != expectActorURIStr {
return gtserror.Newf("actor %s was not the same as expected actor %s",
actorIRIStr, expectActorURIStr)
}
// Extract the object IRI string from Accept.
objectIRIs := ap.GetObjectIRIs(acceptable)
_, objectIRIStr := extractIRI(objectIRIs)
switch {
case objectIRIStr == "":
l.Error("missing Accept object IRI")
return false, nil
// Extract the object IRI from acceptable type.
objectIRI, err := ap.GetOneObjectIRI(acceptable)
if err != nil {
return err
}
// Serialize object ID just once.
objectIRIStr := objectIRI.String()
// Ensure the Accept Object is what we expect
// it to be, ie., it's Accepting the interaction
// we need it to Accept, and not something else.
case objectIRIStr != expectObjectURIStr:
l.Errorf(
"resolved Accept object IRI %s was not the same as expected object %s",
objectIRIStr, expectObjectURIStr,
)
return false, nil
if objectIRIStr != expectObjectURIStr {
return gtserror.Newf("resolved Accept object IRI %s was not the same as expected object %s",
objectIRIStr, expectObjectURIStr)
}
// Extract the target IRI from acceptable type.
targetIRI, err := ap.GetOneTargetIRI(acceptable)
if err != nil {
return err
}
// Serialize target ID just once.
targetIRIStr := targetIRI.String()
// If there's a Target set then verify it's
// what we expect it to be, ie., it should point
// back to the post that's being interacted with.
targetIRIs := ap.GetTargetIRIs(acceptable)
_, targetIRIStr := extractIRI(targetIRIs)
if targetIRIStr != "" && targetIRIStr != expectTargetURIStr {
l.Errorf(
"resolved Accept target IRI %s was not the same as expected target %s",
targetIRIStr, expectTargetURIStr,
)
return false, nil
if targetIRIStr != expectTargetURIStr {
return gtserror.Newf("resolved Accept target IRI %s was not the same as expected target %s",
targetIRIStr, expectTargetURIStr)
}
// Everything looks OK.
return true, nil
// Everything
// looks OK.
return nil
}
func isValidAuthorization(
ctx context.Context,
interactionType gtsmodel.InteractionType,
auth ap.Authorizationable,
authID *url.URL,
expectActorURIStr string, // actor Eg., "https://example.org/users/someone"
expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E"
) (bool, error) {
l := log.
WithContext(ctx).
WithField("auth", authID.String())
) error {
// Check that the type of the Authorization
// matches the interaction it's approving.
@@ -875,83 +868,71 @@ func isValidAuthorization(
// All good baby!
default:
// There's a mismatch.
l.Errorf(
"authorization type %s cannot approve %s",
tn, interactionType.String(),
)
return false, nil
return gtserror.Newf("authorization type %s cannot approve %s",
tn, interactionType.String())
}
// Extract the actor IRI and string from Approval.
actorIRIs := ap.GetAttributedTo(auth)
actorIRI, actorIRIStr := extractIRI(actorIRIs)
switch {
case actorIRIStr == "":
l.Error("authorization missing attributedTo IRI")
return false, nil
// Extract the actor IRI from authorization.
actorIRI, err := ap.GetOneAttributedTo(auth)
if err != nil {
return err
}
// Serialize actor ID just once.
actorIRIStr := actorIRI.String()
// Ensure the authorization actor is on
// the instance hosting the Approval.
case actorIRI.Host != authID.Host:
l.Errorf(
"actor %s not on the same host as authorization",
actorIRIStr,
)
return false, nil
if actorIRI.Host != authID.Host {
return gtserror.Newf("actor %s not on the same host as authorization",
actorIRIStr)
}
// Ensure the auth actor is who we expect
// it to be, and not someone else trying to
// do an auth for an interaction with a
// statusable they don't own.
case actorIRIStr != expectActorURIStr:
l.Errorf(
"actor %s was not the same as expected actor %s",
actorIRIStr, expectActorURIStr,
)
return false, nil
if actorIRIStr != expectActorURIStr {
return gtserror.Newf("actor %s was not the same as expected actor %s",
actorIRIStr, expectActorURIStr)
}
// Extract the object IRI string from authorization.
objectIRIs := ap.GetInteractingObject(auth)
_, objectIRIStr := extractIRI(objectIRIs)
switch {
case objectIRIStr == "":
l.Error("missing authorization interactingObject IRI")
return false, nil
// Extract interacting object IRI from authorization.
objectIRI, err := ap.GetOneInteractingObject(auth)
if err != nil {
return err
}
// Serialize object ID just once.
objectIRIStr := objectIRI.String()
// Ensure the authorization object is what we expect
// it to be, ie., it's approving the interaction
// we need it to approve, and not something else.
case objectIRIStr != expectObjectURIStr:
l.Errorf(
if objectIRIStr != expectObjectURIStr {
return gtserror.Newf(
"resolved authorization interactingObject IRI %s was not the same as expected object %s",
objectIRIStr, expectObjectURIStr,
)
return false, nil
objectIRIStr, expectObjectURIStr)
}
// Extract interaction target IRI from authorization.
targetIRI, err := ap.GetOneInteractionTarget(auth)
if err != nil {
return err
}
// Serialize target ID just once.
targetIRIStr := targetIRI.String()
// Ensure the authorization target is what we expect,
// ie., it should be the status being interacted with.
targetIRIs := ap.GetInteractionTarget(auth)
_, targetIRIStr := extractIRI(targetIRIs)
if targetIRIStr != "" && targetIRIStr != expectTargetURIStr {
l.Errorf(
if targetIRIStr != expectTargetURIStr {
return gtserror.Newf(
"resolved authorization interactionTarget IRI %s was not the same as expected target %s",
targetIRIStr, expectTargetURIStr,
)
return false, nil
targetIRIStr, expectTargetURIStr)
}
// Everything looks OK.
return true, nil
}
// extractIRI is shorthand to extract the first IRI
// url.URL{} object and serialized form from slice.
func extractIRI(iris []*url.URL) (*url.URL, string) {
if len(iris) == 0 {
return nil, ""
}
u := iris[0]
return u, u.String()
// Everything
// looks OK.
return nil
}
@@ -78,8 +78,8 @@ func (d *Dereferencer) GetRelayedStatus(
unlock = util.DoOnce(unlock)
defer unlock()
// Search the database for existing status.
if status, err := d.getStatusDBOnly(ctx, uriStr); err != nil {
// Search the database for an existing status under URI / URL.
if status, err := d.getStatusFromDB(ctx, uriStr); err != nil {
return nil, err
} else if status != nil {
// If we already have the status,
@@ -92,7 +92,8 @@ func (d *Dereferencer) GetRelayedStatus(
// We don't have the relayed status
// stored locally, go dereference it.
status, statusable, err := d.dereferenceRelayableStatus(
ctx, l,
ctx,
&l,
instanceAcct,
relayAcct,
uri,
@@ -193,7 +194,7 @@ func (d *Dereferencer) GetRelayedAnnounce(
return nil, gtserror.SetUnretrievable(err)
}
// Create logger.
// Prepare log entry with fields.
uriStr := boostWrapper.BoostOfURIStr
l := log.
WithContext(ctx).
@@ -204,8 +205,8 @@ func (d *Dereferencer) GetRelayedAnnounce(
unlock = util.DoOnce(unlock)
defer unlock()
// Search the database for existing status.
if status, err := d.getStatusDBOnly(ctx, uriStr); err != nil {
// Search the database for an existing status under URI / URL.
if status, err := d.getStatusFromDB(ctx, uriStr); err != nil {
return nil, err
} else if status != nil {
// If we already have the status,
@@ -218,7 +219,8 @@ func (d *Dereferencer) GetRelayedAnnounce(
// We don't have the relayed status
// stored locally, go dereference it.
status, statusable, err := d.dereferenceRelayableStatus(
ctx, l,
ctx,
&l,
instanceAcct,
relayAcct,
uri,
@@ -333,11 +335,15 @@ func (d *Dereferencer) GetRelayedAnnounce(
func (d *Dereferencer) dereferenceRelayableStatus(
ctx context.Context,
l log.Entry,
l *log.Entry,
instanceAcct *gtsmodel.Account,
relayAcct *gtsmodel.Account,
uri *url.URL,
) (*gtsmodel.Status, ap.Statusable, error) {
) (
*gtsmodel.Status,
ap.Statusable,
error,
) {
// Don't have the status locally, so we need to deref.
//
// Create transport on behalf of our instance account,
@@ -350,20 +356,19 @@ func (d *Dereferencer) dereferenceRelayableStatus(
return nil, nil, gtserror.Newf("couldn't create transport: %w", err)
}
// Dereference statusable from remote, checking
// if we already had this status stored under a
// different URI (ie., final URI after redirects).
statusable, alreadyStatus, err := d.retrieveStatusable(ctx, tsport, uri)
// Dereference statusable from remote, checking if we already had this
// status stored under a different URI (ie., final URI after redirects).
statusable, existing, uri, err := d.retrieveStatusable(ctx, tsport, uri)
if err != nil {
return nil, nil, err
}
// If alreadyStatus was returned, it means
// If existing was returned, it means
// we already had this status stored in the
// db under its final reachable URI and not
// the URI passed in. No need to go further.
if alreadyStatus != nil {
return alreadyStatus, statusable, nil
if existing != nil {
return existing, statusable, nil
}
// We didn't have the status yet + we dereffed it.
@@ -373,7 +378,9 @@ func (d *Dereferencer) dereferenceRelayableStatus(
//
// This will also fetch the status author account.
status, err := d.convertStatusable(ctx,
instanceAcct.Username, uri, statusable,
instanceAcct.Username,
uri,
statusable,
)
if err != nil {
return nil, statusable, err
@@ -382,8 +389,8 @@ func (d *Dereferencer) dereferenceRelayableStatus(
// We're only interested in relayed statuses that
// are public or unlisted, as followers-only statuses
// will be definition by sent to followers anyway.
vis := status.Visibility
if !(vis == gtsmodel.VisibilityPublic ||
if vis := status.Visibility; //
!(vis == gtsmodel.VisibilityPublic ||
vis == gtsmodel.VisibilityUnlocked) {
l.Debug("status neither public nor unlisted")
return nil, statusable, nil
@@ -394,7 +401,6 @@ func (d *Dereferencer) dereferenceRelayableStatus(
// if the author is the same as for this status.
var inReplyToAccountURI string
if status.InReplyToURI != "" {
var err error
inReplyToAccountURI, err = d.retrieveInReplyToAccountURI(ctx, tsport, status)
if err != nil {
return nil, statusable, err
@@ -466,9 +472,8 @@ func (d *Dereferencer) retrieveInReplyToAccountURI(
return "", nil
}
// We don't have the parent stored, try to fetch but
// *don't* store it, we only want to check for now.
parentStatusable, parentStatus, err := d.retrieveStatusable(ctx, tsport, parentURI)
// We don't have the parent stored, try to fetch but *don't* store it, only check for now.
parentStatusable, parentStatus, _, err := d.retrieveStatusable(ctx, tsport, parentURI)
if err != nil {
err := gtserror.Newf("error retrieving %s: %w", inReplyToURI, err)
return "", err
@@ -480,11 +485,11 @@ func (d *Dereferencer) retrieveInReplyToAccountURI(
return parentStatus.AccountURI, nil
}
attributedTo := ap.GetAttributedTo(parentStatusable)
if len(attributedTo) == 0 {
err := gtserror.Newf("parent %s had no attributedTo", inReplyToURI)
return "", err
// Get attributedTo URI from the dereferenced parent status.
attributedTo, err := ap.GetOneAttributedTo(parentStatusable)
if err != nil {
return "", gtserror.SetMalformed(err)
}
return attributedTo[0].String(), nil
return attributedTo.String(), nil
}
@@ -237,7 +237,7 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() {
testrig.URLMustParse(remoteAltURI),
)
expectErrStr := fmt.Sprintf(
"retrieveStatusable: http URI %s does not match dereferenced statusable id or url(s) [%s %s]",
"retrieveStatusable: fetch uri %s does not match known status uri(s): [%s,%s]",
remoteAltURI, remoteURL, remoteURI,
)
suite.Equal(expectErrStr, err.Error())
+9 -12
View File
@@ -625,22 +625,20 @@ func (f *DB) parseAcceptInteractionRequestable(
// Ensure we have actor IRI on
// the interaction requestable.
actors := ap.GetActorIRIs(intRequestable)
if len(actors) != 1 {
actorURI, err := ap.GetOneActorIRI(intRequestable)
if err != nil {
const text = "invalid or missing actor property on embedded interaction request"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
actorURI := actors[0]
// Ensure we have an object URI, which
// should point to the statusable being
// interacted with, ie., the parent status.
objects := ap.GetObjectIRIs(intRequestable)
if len(objects) != 1 {
parentURI, err := ap.GetOneObjectIRI(intRequestable)
if err != nil {
const text = "invalid or missing object property on embedded interaction request"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
parentURI := objects[0]
// Ensure we have instrument, which should
// be or point to the activity/object that
@@ -669,12 +667,11 @@ func (f *DB) parseAcceptInteractionRequestable(
// Ensure we have result URI, which should
// point to an authorization for this interaction.
results := ap.GetResultIRIs(accept)
if len(results) != 1 {
authURI, err := ap.GetOneResultIRI(accept)
if err != nil {
const text = "invalid or missing result property on embedded interaction request"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
authURI := results[0]
// Check if we have a gtsmodel interaction
// request already stored for this interaction.
+5 -1
View File
@@ -375,7 +375,11 @@ func (f *DB) createStatusable(
forwarded bool,
) error {
// Check for spam / relevance.
ok, err := f.statusableOK(ctx, receiver, requester, statusable)
ok, err := f.statusableOK(ctx,
receiver,
requester,
statusable,
)
if err != nil {
// Error already
// wrapped.
@@ -398,9 +398,8 @@ func (f *DB) ReplyRequest(ctx context.Context, replyReq vocab.GoToSocialReplyReq
}
// Extract the attributed to (i.e. author) URI of status.
attributedToURI, err := ap.ExtractAttributedToURI(statusable)
attributedToURI, err := ap.GetOneAttributedTo(statusable)
if err != nil {
err := gtserror.Newf("invalid status attributedTo value: %w", err)
return gtserror.WrapWithCode(http.StatusBadRequest, err)
}
+8 -8
View File
@@ -127,8 +127,8 @@ func (f *DB) undoFollow(
// Make sure the Undo
// actor owns the target.
if !sameActor(
undo.GetActivityStreamsActor(),
asFollow.GetActivityStreamsActor(),
undo,
asFollow,
) {
// Ignore this Activity.
return nil
@@ -204,8 +204,8 @@ func (f *DB) undoLike(
// Make sure the Undo
// actor owns the target.
if !sameActor(
undo.GetActivityStreamsActor(),
asLike.GetActivityStreamsActor(),
undo,
asLike,
) {
// Ignore this Activity.
return nil
@@ -304,8 +304,8 @@ func (f *DB) undoBlock(
// Make sure the Undo
// actor owns the target.
if !sameActor(
undo.GetActivityStreamsActor(),
asBlock.GetActivityStreamsActor(),
undo,
asBlock,
) {
// Ignore this Activity.
return nil
@@ -374,8 +374,8 @@ func (f *DB) undoAnnounce(
// Make sure the Undo actor owns the
// Announce they're trying to undo.
if !sameActor(
undo.GetActivityStreamsActor(),
asAnnounce.GetActivityStreamsActor(),
undo,
asAnnounce,
) {
// Ignore this Activity.
return nil
+7 -29
View File
@@ -52,40 +52,18 @@ func typeNames(objects []ap.TypeOrIRI) []string {
// isSender returns whether an object with AttributedTo property comes from the given requesting account.
func isSender(with ap.WithAttributedTo, requester *gtsmodel.Account) bool {
for _, uri := range ap.GetAttributedTo(with) {
if uri.String() == requester.URI {
return true
}
}
return false
uri, _ := ap.GetOneAttributedTo(with)
return uri != nil && uri.String() == requester.URI
}
func sameActor(actor1 vocab.ActivityStreamsActorProperty, actor2 vocab.ActivityStreamsActorProperty) bool {
func sameActor(actor1 ap.WithActor, actor2 ap.WithActor) bool {
if actor1 == nil || actor2 == nil {
return false
}
for a1Iter := actor1.Begin(); a1Iter != actor1.End(); a1Iter = a1Iter.Next() {
a1IRI := a1Iter.GetIRI()
if a1IRI == nil {
return false
}
a1IRIStr := a1IRI.String()
for a2Iter := actor2.Begin(); a2Iter != actor2.End(); a2Iter = a2Iter.Next() {
a2IRI := a2Iter.GetIRI()
if a2IRI == nil {
return false
}
a2IRIStr := a2IRI.String()
if a1IRIStr == a2IRIStr {
return true
}
}
}
return false
actor1URI, _ := ap.GetOneActorIRI(actor1)
actor2URI, _ := ap.GetOneActorIRI(actor2)
return actor1URI != nil && actor2URI != nil &&
actor1URI.String() == actor2URI.String()
}
// NewID creates a new IRI id for the provided activity or object. The
+14 -16
View File
@@ -62,26 +62,26 @@ import (
// to the ResponseWriter as is expected that the caller to PostInbox will
// do so when handling the error.
func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
// Extract any other IRIs involved in this activity.
otherIRIs := []*url.URL{}
// Extract any other IRIs
// involved in this activity.
var otherIRIs []*url.URL
// Get the ID of the Activity itslf.
activityID, err := pub.GetId(activity)
if err == nil {
activityID, _ := pub.GetId(activity)
if activityID != nil {
otherIRIs = append(otherIRIs, activityID)
}
// Check if the Activity has an 'inReplyTo'.
if replyToable, ok := activity.(ap.ReplyToable); ok {
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
otherIRIs = append(otherIRIs, inReplyToURI)
}
otherIRIs = append(otherIRIs, ap.GetInReplyTo(replyToable)...)
}
// Check for TO and CC URIs on the Activity.
if addressable, ok := activity.(ap.Addressable); ok {
otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...)
otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...)
otherIRIs = append(otherIRIs, ap.GetTo(addressable)...)
otherIRIs = append(otherIRIs, ap.GetCc(addressable)...)
}
// Now perform the same checks, but
@@ -99,20 +99,18 @@ func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques
continue
}
objectID, err := pub.GetId(t)
if err == nil {
objectID, _ := pub.GetId(t)
if objectID != nil {
otherIRIs = append(otherIRIs, objectID)
}
if replyToable, ok := t.(ap.ReplyToable); ok {
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
otherIRIs = append(otherIRIs, inReplyToURI)
}
otherIRIs = append(otherIRIs, ap.GetInReplyTo(replyToable)...)
}
if addressable, ok := t.(ap.Addressable); ok {
otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...)
otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...)
otherIRIs = append(otherIRIs, ap.GetTo(addressable)...)
otherIRIs = append(otherIRIs, ap.GetCc(addressable)...)
}
}
}
-6
View File
@@ -26,7 +26,6 @@ import (
"time"
"code.superseriousbusiness.org/gotosocial/internal/config"
"codeberg.org/gruf/go-bytesize"
"codeberg.org/gruf/go-fastcopy"
"codeberg.org/gruf/go-storage"
"codeberg.org/gruf/go-storage/disk"
@@ -107,11 +106,6 @@ func NewFileStorage() (*Driver, error) {
// Load runtime configuration
basePath := config.GetStorageLocalBasePath()
// Update fastcopy global buffer pool
// to use our requested buffer size.
const bufsize = 16 * bytesize.KiB
fastcopy.Buffer(int(bufsize))
// Use default disk config with
// increased write buffer size.
diskCfg := disk.DefaultConfig()
+29 -39
View File
@@ -381,7 +381,6 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// Account that created the status. Assume we have this
// in the db by the time this function is called, else error.
status.Account, err = c.getASAttributedToAccount(ctx,
status.URI,
statusable,
)
if err != nil {
@@ -495,12 +494,12 @@ func (c *Converter) ASFollowToFollowRequest(ctx context.Context, followable ap.F
// Stringify uri obj.
uri := uriObj.String()
origin, err := c.getASActorAccount(ctx, uri, followable)
origin, err := c.getASActorAccount(ctx, followable)
if err != nil {
return nil, err
}
target, err := c.getASObjectAccount(ctx, uri, followable)
target, err := c.getASObjectAccount(ctx, followable)
if err != nil {
return nil, err
}
@@ -525,12 +524,12 @@ func (c *Converter) ASFollowToFollow(ctx context.Context, followable ap.Followab
// Stringify uri obj.
uri := uriObj.String()
origin, err := c.getASActorAccount(ctx, uri, followable)
origin, err := c.getASActorAccount(ctx, followable)
if err != nil {
return nil, err
}
target, err := c.getASObjectAccount(ctx, uri, followable)
target, err := c.getASObjectAccount(ctx, followable)
if err != nil {
return nil, err
}
@@ -557,12 +556,12 @@ func (c *Converter) ASLikeToFave(ctx context.Context, likeable ap.Likeable) (*gt
// Stringify uri obj.
uri := uriObj.String()
origin, err := c.getASActorAccount(ctx, uri, likeable)
origin, err := c.getASActorAccount(ctx, likeable)
if err != nil {
return nil, err
}
target, err := c.getASObjectStatus(ctx, uri, likeable)
target, err := c.getASObjectStatus(ctx, likeable)
if err != nil {
return nil, err
}
@@ -593,12 +592,12 @@ func (c *Converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable)
// Stringify uri obj.
uri := uriObj.String()
origin, err := c.getASActorAccount(ctx, uri, blockable)
origin, err := c.getASActorAccount(ctx, blockable)
if err != nil {
return nil, err
}
target, err := c.getASObjectAccount(ctx, uri, blockable)
target, err := c.getASObjectAccount(ctx, blockable)
if err != nil {
return nil, err
}
@@ -677,7 +676,6 @@ func (c *Converter) ASAnnounceToStatus(
// Extract and load the boost actor account,
// (this MUST already be in database by now).
boost.Account, err = c.getASActorAccount(ctx,
uri,
announceable,
)
if err != nil {
@@ -727,7 +725,7 @@ func (c *Converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable)
uri := uriObj.String()
// Extract the origin (actor) account for report.
origin, err := c.getASActorAccount(ctx, uri, flaggable)
origin, err := c.getASActorAccount(ctx, flaggable)
if err != nil {
return nil, err
}
@@ -852,16 +850,14 @@ func (c *Converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable)
}, nil
}
func (c *Converter) getASActorAccount(ctx context.Context, id string, with ap.WithActor) (*gtsmodel.Account, error) {
// Get actor IRIs from type.
actor := ap.GetActorIRIs(with)
if len(actor) == 0 {
err := gtserror.Newf("unusable actor property iri for %s", id)
func (c *Converter) getASActorAccount(ctx context.Context, with ap.WithActor) (*gtsmodel.Account, error) {
actorIRI, err := ap.GetOneActorIRI(with)
if err != nil {
return nil, gtserror.SetMalformed(err)
}
// Check for account in database with provided actor URI.
account, err := c.state.DB.GetAccountByURI(ctx, actor[0].String())
// Check for account in database with the extracted actor URI.
account, err := c.state.DB.GetAccountByURI(ctx, actorIRI.String())
if err != nil {
return nil, gtserror.Newf("error getting actor account from database: %w", err)
}
@@ -869,16 +865,14 @@ func (c *Converter) getASActorAccount(ctx context.Context, id string, with ap.Wi
return account, nil
}
func (c *Converter) getASAttributedToAccount(ctx context.Context, id string, with ap.WithAttributedTo) (*gtsmodel.Account, error) {
// Get attribTo IRIs from type.
attribTo := ap.GetAttributedTo(with)
if len(attribTo) == 0 {
err := gtserror.Newf("unusable attributedTo property iri for %s", id)
func (c *Converter) getASAttributedToAccount(ctx context.Context, with ap.WithAttributedTo) (*gtsmodel.Account, error) {
attribTo, err := ap.GetOneAttributedTo(with)
if err != nil {
return nil, gtserror.SetMalformed(err)
}
// Check for account in database with provided attributedTo URI.
account, err := c.state.DB.GetAccountByURI(ctx, attribTo[0].String())
// Check for account in database with extracted attributedTo URI.
account, err := c.state.DB.GetAccountByURI(ctx, attribTo.String())
if err != nil {
return nil, gtserror.Newf("error getting actor account from database: %w", err)
}
@@ -886,16 +880,14 @@ func (c *Converter) getASAttributedToAccount(ctx context.Context, id string, wit
return account, nil
}
func (c *Converter) getASObjectAccount(ctx context.Context, id string, with ap.WithObject) (*gtsmodel.Account, error) {
// Get object IRIs from type.
object := ap.GetObjectIRIs(with)
if len(object) == 0 {
err := gtserror.Newf("unusable object property iri for %s", id)
func (c *Converter) getASObjectAccount(ctx context.Context, with ap.WithObject) (*gtsmodel.Account, error) {
objectIRI, err := ap.GetOneObjectIRI(with)
if err != nil {
return nil, gtserror.SetMalformed(err)
}
// Check for account in database with provided object URI.
account, err := c.state.DB.GetAccountByURI(ctx, object[0].String())
// Check for account in database with the extracted object URI.
account, err := c.state.DB.GetAccountByURI(ctx, objectIRI.String())
if err != nil {
return nil, gtserror.Newf("error getting object account from database: %w", err)
}
@@ -903,16 +895,14 @@ func (c *Converter) getASObjectAccount(ctx context.Context, id string, with ap.W
return account, nil
}
func (c *Converter) getASObjectStatus(ctx context.Context, id string, with ap.WithObject) (*gtsmodel.Status, error) {
// Get object IRIs from type.
object := ap.GetObjectIRIs(with)
if len(object) == 0 {
err := gtserror.Newf("unusable object property iri for %s", id)
func (c *Converter) getASObjectStatus(ctx context.Context, with ap.WithObject) (*gtsmodel.Status, error) {
objectIRI, err := ap.GetOneObjectIRI(with)
if err != nil {
return nil, gtserror.SetMalformed(err)
}
// Check for status in database with provided object URI.
status, err := c.state.DB.GetStatusByURI(ctx, object[0].String())
// Check for status in database with the extracted object URI.
status, err := c.state.DB.GetStatusByURI(ctx, objectIRI.String())
if err != nil {
return nil, gtserror.Newf("error getting object status from database: %w", err)
}
+3
View File
@@ -0,0 +1,3 @@
# buffers
this provides global instances of configurable (2^n size), shared byte buffer memory pools.
+91
View File
@@ -0,0 +1,91 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package buffers
import (
"math"
"sync"
"unsafe"
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-mempool"
)
var (
// global map of memory pool instances.
buffers = make(map[uint32]*MemoryPool, 8)
// global map lock.
mutex sync.Mutex
)
// MemoryPool is a memory pool of
// byte buffers, of a predefined size.
//
// This will be a shared global instance that
// any callers of buffers.Pool($sz) can access.
type MemoryPool struct {
p mempool.UnsafePool
s uint32
}
// Pool returns a shared MemoryPool instance
// of requested size (rounded to nearest 2^n).
//
// NOTE: this acquires a lock on a global mutex
// instance and should generally only be called
// on package init to store a global reference.
func Pool(sz uint32) *MemoryPool {
n := math.Log2(float64(sz))
n = math.Round(n)
n = math.Exp2(max(8, n))
sz = uint32(n)
mutex.Lock()
p := buffers[sz]
if p == nil {
p = new(MemoryPool)
p.s = sz
buffers[sz] = p
}
mutex.Unlock()
return p
}
// Get returns a byteutil.Buffer{} instance from pool.
func (p *MemoryPool) Get() *byteutil.Buffer {
buf := (*byteutil.Buffer)(p.p.Get())
if buf == nil {
buf = new(byteutil.Buffer)
buf.B = make([]byte, p.s)
} else {
clear(buf.B[0:cap(buf.B)])
}
buf.B = buf.B[:0]
return buf
}
// Put replaces byteutil.Buffer{} instance in pool.
func (p *MemoryPool) Put(buf *byteutil.Buffer) {
if buf == nil {
return
}
if cap(buf.B) < int(p.s) || cap(buf.B) > 2*int(p.s) {
return // drop buffers outside size range
}
p.p.Put(unsafe.Pointer(buf))
}
+77
View File
@@ -0,0 +1,77 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package buffers
import (
"io"
"unsafe"
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-fastcopy"
)
// global fastcopy instance.
var copypool = fastcopy.Global()
func init() {
// Get new pool.
p := Pool(16384)
// Set fastcopy.CopyPool{} getter and setter functions, ensuring buffer is set to max size.
copypool.Get = func() *[]byte { buf := p.Get(); buf.B = buf.Full(); return toBytes(buf) }
copypool.Put = func(b *[]byte) { p.Put(toBuffer(b)) }
}
// Copy: see fastcopy.CopyPool{}.Copy().
func Copy(dst io.Writer, src io.Reader) (int64, error) {
return copypool.Copy(dst, src)
}
// CopyN: see fastcopy.CopyPool{}.CopyN().
func CopyN(dst io.Writer, src io.Reader, n int64) (int64, error) {
return copypool.CopyN(dst, src, n)
}
// CopyBuffer: see fastcopy.CopyBuffer().
func CopyBuffer(dst io.Writer, src io.Reader, buf *byteutil.Buffer) (int64, error) {
return fastcopy.CopyBuffer(dst, src, toBytes(buf))
}
// toBytes casts buffer to bytes with compile-time assertion,
// this allows us to use our byteutil.Buffer{} pool for []bytes too.
// this is only possible due to the confirmed memory semantics below.
func toBytes(buf *byteutil.Buffer) (b *[]byte) {
if unsafe.Sizeof(buf) != unsafe.Sizeof(b) ||
unsafe.Offsetof(buf.B) != 0 {
panic("compile time assertion")
}
b = (*[]byte)(unsafe.Pointer(buf))
return
}
// toBuffer casts bytes to buffer with compile-time assertion,
// this allows us to use our byteutil.Buffer{} pool for []bytes too.
// this is only possible due to the confirmed memory semantics below.
func toBuffer(b *[]byte) (buf *byteutil.Buffer) {
if unsafe.Sizeof(buf) != unsafe.Sizeof(b) ||
unsafe.Offsetof(buf.B) != 0 {
panic("compile time assertion")
}
buf = (*byteutil.Buffer)(unsafe.Pointer(b))
return
}
+2 -2
View File
@@ -1,5 +1,5 @@
# log
An opinionated logging package to be initialized once on server initialization, and with a `context.Context` almost always required.
An opinionated logging package expecting to be configured **once** on server initialization, and with a `context.Context` almost always required.
Formatting with the `logfmt` function is handled by `codeberg.org/gruf/go-kv/v2/format`, which is significantly faster and more useful in map / struct formatting than the standard library pkg.
Formatting with the `logfmt` function is handled by `codeberg.org/gruf/go-kv/v2/format`, which is significantly faster and more useful in map / struct formatting than the standard library pkg.
@@ -18,32 +18,24 @@
package log
import (
"unsafe"
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-mempool"
"codeberg.org/gruf/go-kv/v2/format"
)
// memory pool of log buffers.
var bufpool mempool.UnsafePool
// LogFormatted provides log formatting
// of wrapped interface via String() method.
type LogFormatted struct{ any }
// getBuf acquires a buffer from memory pool.
func getBuf() *byteutil.Buffer {
buf := (*byteutil.Buffer)(bufpool.Get())
if buf == nil {
buf = new(byteutil.Buffer)
buf.B = make([]byte, 0, 512)
}
return buf
// String: implements fmt.Stringer{}.
func (f LogFormatted) String() string {
buf := bufpool.Get()
buf.B = format.Global.Append(buf.B, f.any, argArgs)
str := string(buf.B)
bufpool.Put(buf)
return str
}
// putBuf places (after resetting) buffer back in
// memory pool, dropping if capacity too large.
func putBuf(buf *byteutil.Buffer) {
if cap(buf.B) > int(^uint16(0)) {
return // drop large buffer
}
buf.B = buf.B[:0]
ptr := unsafe.Pointer(buf)
bufpool.Put(ptr)
// Formatted wraps value in LogFormatted{}
// for nicer formatting via String() method.
func Formatted(v any) LogFormatted {
return LogFormatted{v}
}
+6 -2
View File
@@ -24,6 +24,7 @@ import (
"runtime"
"time"
"code.superseriousbusiness.org/gopkg/buffers"
"code.superseriousbusiness.org/gopkg/log/format"
"code.superseriousbusiness.org/gopkg/log/level"
"code.superseriousbusiness.org/gopkg/xslices"
@@ -45,6 +46,9 @@ var state = struct {
},
}
// memory pool of log buffers.
var bufpool = buffers.Pool(1024)
// Level returns the
// currently set log.
func Level() LEVEL {
@@ -391,8 +395,8 @@ func logf(ctx context.Context, lvl LEVEL, fields []kv.Field, msg string, args ..
_ = runtime.Callers(3, pcs)
// Acquire buffer.
buf := getBuf()
defer putBuf(buf)
buf := bufpool.Get()
defer bufpool.Put(buf)
if ctx != nil && len(state.hooks) > 0 {
// Ensure fields have space for our context hooks.
+13
View File
@@ -66,6 +66,19 @@ func Name(pc uintptr) string {
// Get will return calling func information for given PC value,
// caching func names by their PC values to reduce calls to Caller().
func Get(pc uintptr) string {
ptr := callerCache.Load()
if ptr != nil {
name, ok := (*ptr)[pc]
if ok {
return name
}
}
return get_slow(pc)
}
// get_slow is the outlined version of the slow
// parts of Get() in order to inline the fast parts.
func get_slow(pc uintptr) string {
var cache map[uintptr]string
for {
// Load caller cache map.
+74 -52
View File
@@ -3,106 +3,125 @@ package fastcopy
import (
"errors"
"io"
"sync"
_ "unsafe" // link to io.errInvalidWrite.
)
var (
// global pool instance.
pool = CopyPool{size: 4096}
// global pool.
global CopyPool
// errInvalidWrite means that a write returned an impossible count.
errInvalidWrite = errors.New("invalid write result")
)
// CopyPool provides a memory pool of byte
// buffers for io copies from readers to writers.
type CopyPool struct {
size int
pool sync.Pool
}
// See CopyPool.Buffer().
func Buffer(sz int) int {
return pool.Buffer(sz)
}
// Global returns global CopyPool{} instance.
func Global() *CopyPool { return &global }
// See CopyPool.CopyN().
func CopyN(dst io.Writer, src io.Reader, n int64) (int64, error) {
return pool.CopyN(dst, src, n)
return global.CopyN(dst, src, n)
}
// See CopyPool.Copy().
func Copy(dst io.Writer, src io.Reader) (int64, error) {
return pool.Copy(dst, src)
return global.Copy(dst, src)
}
// Buffer sets the pool buffer size to allocate. Returns current size.
// Note this is NOT atomically safe, please call BEFORE other calls to CopyPool.
func (cp *CopyPool) Buffer(sz int) int {
if sz > 0 {
// update size
cp.size = sz
} else if cp.size < 1 {
// default size
return 4096
// CopyPool provides a memory pool of byte
// buffers for io copies from read -> writer.
type CopyPool struct {
Get func() *[]byte
Put func(*[]byte)
}
// get attempts to fetch buffer from
// currently-set CopyPool{}.Get() func.
func (cp *CopyPool) get() *[]byte {
if cp.Get != nil {
return cp.Get()
}
return nil
}
// put attempts to replace buffer into
// currently-set CopyPool{}.Put() func.
func (cp *CopyPool) put(buf *[]byte) {
if cp.Put != nil {
cp.Put(buf)
}
return cp.size
}
// CopyN performs the same logic as io.CopyN(), with the difference
// being that the byte buffer is acquired from a memory pool.
// being that the byte buffer is acquired from a memory pool if required.
func (cp *CopyPool) CopyN(dst io.Writer, src io.Reader, n int64) (int64, error) {
written, err := cp.Copy(dst, io.LimitReader(src, n))
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
// src stopped early;
// must have been EOF.
err = io.EOF
}
return written, err
}
// Copy performs the same logic as io.Copy(), with the difference
// being that the byte buffer is acquired from a memory pool.
// being that the byte buffer is acquired from a memory pool if required.
func (cp *CopyPool) Copy(dst io.Writer, src io.Reader) (int64, error) {
// Prefer using io.WriterTo to do the copy (avoids alloc + copy)
// Prefer using io.WriterTo to do
// the copy (avoids alloc + copy).
if wt, ok := src.(io.WriterTo); ok {
return wt.WriteTo(dst)
}
// Prefer using io.ReaderFrom to do the copy.
// Prefer using io.ReaderFrom to copy.
if rt, ok := dst.(io.ReaderFrom); ok {
return rt.ReadFrom(src)
}
var buf []byte
// Acquire buf.
buf := cp.get()
if b, ok := cp.pool.Get().(*[]byte); ok {
// Acquired buf from pool
buf = *b
} else {
// Allocate new buffer of size
buf = make([]byte, cp.Buffer(0))
// Perform copy operation with buf.
n, err := CopyBuffer(dst, src, buf)
// Release.
cp.put(buf)
return n, err
}
// CopyBuffer performs the same logic as io.Copy(), without any optimized
// checks for io.WriterTo{} or io.ReaderFrom{}. Data will definitively be
// copied from source to destination using the given byte buffer. This allows
// callers to wrap this with flexible buffer sourcing, knowing it will be used.
func CopyBuffer(dst io.Writer, src io.Reader, buf *[]byte) (int64, error) {
switch {
case dst == nil:
panic("nil dst")
case src == nil:
panic("nil src")
case buf == nil || len(*buf) == 0:
new := make([]byte, 4096)
buf = &new
default:
// Ensure full buf available.
(*buf) = (*buf)[0:cap(*buf)]
}
// Defer release to pool
defer cp.pool.Put(&buf)
var n int64
for {
// Perform next read into buf
nr, err := src.Read(buf)
// Perform read into buf.
nr, err := src.Read(*buf)
if nr > 0 {
// We error check AFTER checking
// no. read bytes so incomplete
// read still gets written up to nr.
// Perform next write from buf
nw, ew := dst.Write(buf[0:nr])
// Perform next write from buf.
nw, ew := dst.Write((*buf)[0:nr])
// Check for valid write
// Check for valid write.
if nw < 0 || nr < nw {
if ew == nil {
ew = errInvalidWrite
@@ -110,21 +129,24 @@ func (cp *CopyPool) Copy(dst io.Writer, src io.Reader) (int64, error) {
return n, ew
}
// Incr total count
// Incr total.
n += int64(nw)
// Check write error
// Check for
// write error
if ew != nil {
return n, ew
}
// Check unequal read/writes
// Check unequal
// read / writes.
if nr != nw {
return n, io.ErrShortWrite
}
}
// Return on err
// Return on
// any set error.
if err != nil {
if err == io.EOF {
err = nil // expected
+5 -4
View File
@@ -213,8 +213,9 @@ code.superseriousbusiness.org/go-jpeg-image-structure/v2
# code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0
## explicit; go 1.12
code.superseriousbusiness.org/go-png-image-structure/v2
# code.superseriousbusiness.org/gopkg v0.0.0-20260520114201-7a01c90db818
## explicit; go 1.24.6
# code.superseriousbusiness.org/gopkg v0.0.0-20260623123713-4ead213a2d22
## explicit; go 1.25.0
code.superseriousbusiness.org/gopkg/buffers
code.superseriousbusiness.org/gopkg/log
code.superseriousbusiness.org/gopkg/log/format
code.superseriousbusiness.org/gopkg/log/level
@@ -242,7 +243,7 @@ codeberg.org/gruf/go-byteutil
codeberg.org/gruf/go-cache/v3
codeberg.org/gruf/go-cache/v3/simple
codeberg.org/gruf/go-cache/v3/ttl
# codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf
# codeberg.org/gruf/go-caller v0.0.0-20260615202027-bc5a6ff3cb31
## explicit; go 1.24.5
codeberg.org/gruf/go-caller
# codeberg.org/gruf/go-debug v1.3.0
@@ -251,7 +252,7 @@ codeberg.org/gruf/go-debug
# codeberg.org/gruf/go-errors/v2 v2.3.4
## explicit; go 1.24.5
codeberg.org/gruf/go-errors/v2
# codeberg.org/gruf/go-fastcopy v1.1.3
# codeberg.org/gruf/go-fastcopy v1.2.1
## explicit; go 1.17
codeberg.org/gruf/go-fastcopy
# codeberg.org/gruf/go-fastpath/v2 v2.0.1