Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encoding: 'omitempty' will panic if the struct contains a uncomparable type #360

Closed
ybaldus opened this issue Jul 28, 2022 · 16 comments · Fixed by #361
Closed

Encoding: 'omitempty' will panic if the struct contains a uncomparable type #360

ybaldus opened this issue Jul 28, 2022 · 16 comments · Fixed by #361

Comments

@ybaldus
Copy link

ybaldus commented Jul 28, 2022

While encoding a struct will be checked for emptiness with the isEmpty function:

func isEmpty(rv reflect.Value) bool {
	switch rv.Kind() {
	case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
		return rv.Len() == 0
	case reflect.Struct:
		return reflect.Zero(rv.Type()).Interface() == rv.Interface()
	case reflect.Bool:
		return !rv.Bool()
	}
	return false
}

The comparing of structs panics if the structs contains any uncomparable type.

Minimal example:

type a struct {
	B b `toml:"B,omitempty"`
}

type b struct {
	C []string `toml:"C,omitempty"`
}

func TestToml(t *testing.T) {
	b1 := b{C: []string{"foo"}}
	a1 := a{B: b1}
	var buf bytes.Buffer
	toml.NewEncoder(&buf).Encode(a1)
	fmt.Printf("%s", buf.String())
}

Output:

=== RUN   TestToml
--- FAIL: TestToml (0.00s)
panic: runtime error: comparing uncomparable type data.b [recovered]
	panic: runtime error: comparing uncomparable type data.b [recovered]
	panic: runtime error: comparing uncomparable type data.b
@arp242
Copy link
Collaborator

arp242 commented Jul 28, 2022

Thanks for the reproducible example; this should be fixed by #361: it will return an error now instead.

@ybaldus
Copy link
Author

ybaldus commented Jul 28, 2022

Thanks for the fast fix. Is it correct to return an error here? Atleast that would be a breaking change because with 1.1.0 it was possible to encode those structs. Now everyone has to check every struct for compareable types before useing an omitempty flag.

@arp242
Copy link
Collaborator

arp242 commented Jul 28, 2022

Wouldn't it be surprising if you add omitempty and it silently gets ignored? The way I imagined it, is that the omitempty should be removed from your struct, rather than doing a IsComparable check in your code.

Breaking compatibility of course isn't great, but it seems to me that adding omitempty here is essentially a bug in your code.

We could ignore it, or maybe add a Strict option or something.

I'll reopen this issue for now to discus.

@arp242 arp242 reopened this Jul 28, 2022
@arp242
Copy link
Collaborator

arp242 commented Jul 28, 2022

Would probably be best to see how encoding/{json,xml} behave for this, and then copy that. I typically try to follow the stdlib encoding's lead unless there's a good reason not to, as it avoids confusion.

@ybaldus
Copy link
Author

ybaldus commented Jul 28, 2022

Maybe you understood me wrong but I wouldn't ignore it silently. I wanted to say that it should be checked field by field if the struct is uncomparable instead of returning an error. I'm pretty sure the stdlib does it that way. I'm not sure about the best/efficient way to check if a struct is empty.

https://cs.opensource.google/go/go/+/master:src/encoding/json/encode.go;l=731;drc=424814615491c604e6a0945f33e5a7b779dc2be5

I think the stdlib always compares on a field basis if my understanding of that code is correct.

arp242 added a commit that referenced this issue Jul 28, 2022
…efore

This is more compatible with the previous behaviour, as well as what
stdlib does.

Ref: #360
@arp242
Copy link
Collaborator

arp242 commented Jul 28, 2022

I wanted to say that it should be checked field by field if the struct is uncomparable instead of returning an error. I'm pretty sure the stdlib does it that way.

The standard library doesn't do anything for structs; omitempty on those seems silently ignored (no error). omitempty support for structs was added in the last release of this library. This is the equivalent of isEmpty(): https://cs.opensource.google/go/go/+/master:src/encoding/json/encode.go;drc=462b78fe7027ef0d2e2b40c3cfd1f5a37d307310;l=340

So I guess that ignoring it is better.

Support for some of these types could be added by looping over all struct fields and recursively calling isEmpty() on them as long as the type isn't a struct. Something like the below seems to work in a quick test, but need to look at it more.

For now, I just changed it to silently ignore these fields.

diff --git encode.go encode.go
index 6b41533..6bfae83 100644
--- encode.go
+++ encode.go
@@ -653,13 +653,20 @@ func (enc *Encoder) isEmpty(rv reflect.Value) bool {
        switch rv.Kind() {
        case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
                return rv.Len() == 0
-       case reflect.Struct:
-               if !rv.Type().Comparable() {
-                       encPanic(fmt.Errorf("type %q cannot be used with omitempty as it's uncomparable", rv.Type()))
-               }
-               return reflect.Zero(rv.Type()).Interface() == rv.Interface()
        case reflect.Bool:
                return !rv.Bool()
+       case reflect.Struct:
+               if rv.Type().Comparable() {
+                       return reflect.Zero(rv.Type()).Interface() == rv.Interface()
+               }
+               // Not comparable; need to loop over all fields and call isEmpty()
+               // recursively so that we check the length for slices etc.
+               for i := 0; i < rv.NumField(); i++ {
+                       if enc.isEmpty(rv.Field(i)) {
+                               return true
+                       }
+               }
+               return false
        }
        return false
 }

@ybaldus
Copy link
Author

ybaldus commented Jul 30, 2022

Yes of course you are right. They will print out an empty object. Can I contribute anything to help you?

@arp242
Copy link
Collaborator

arp242 commented Jul 30, 2022

Can I contribute anything to help you?

Sure! Can make a PR based on the above with a bunch of test cases (including deeply nested structs and the like).

@arp242
Copy link
Collaborator

arp242 commented Aug 2, 2022

Also see the discussion over here regarding the implementation: #361 (comment)

@ybaldus
Copy link
Author

ybaldus commented Aug 2, 2022

I will check that. This week is unfortunately a little bit busy, so I will probably do it next week.

@arp242
Copy link
Collaborator

arp242 commented Aug 2, 2022

Sure, don't feel obliged :-)

@arp242
Copy link
Collaborator

arp242 commented Aug 5, 2022

#362

ondrejbudai added a commit to ondrejbudai/osbuild-composer that referenced this issue Aug 28, 2022
See BurntSushi/toml#360

A recent change in BurntSushi/toml made encoding fail (later changed to
error) if a struct is marked as omitempty and is comparable. Go docs about
equality: https://go.dev/doc/go1#equality. Basically: A struct is comparable
if all of its fields are comparable. Slices are not comparable.

Customizations are marked as omitempty but they contain a lot of slices,
thus they are not comparable. The new version of BurntSushi/toml therefore
panics when we encode them.

The solution is to remove the omitempty tag from Customizations.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
thozza pushed a commit to osbuild/osbuild-composer that referenced this issue Aug 29, 2022
See BurntSushi/toml#360

A recent change in BurntSushi/toml made encoding fail (later changed to
error) if a struct is marked as omitempty and is comparable. Go docs about
equality: https://go.dev/doc/go1#equality. Basically: A struct is comparable
if all of its fields are comparable. Slices are not comparable.

Customizations are marked as omitempty but they contain a lot of slices,
thus they are not comparable. The new version of BurntSushi/toml therefore
panics when we encode them.

The solution is to remove the omitempty tag from Customizations.

Signed-off-by: Ondřej Budai <ondrej@budai.cz>
@joukewitteveen
Copy link

I believe there is an additional issue where isEmpty is called on a fully dereferenced struct, but if the original field type of the struct was a pointer to a struct, then omitempty should be taken to mean "drop if nil, otherwise output".

In other words: isEmpty on any pointer type should return true precisely when the pointer is nil. This is currently not what happens because the field value is fully dereferenced.

@arp242
Copy link
Collaborator

arp242 commented Oct 22, 2022

Fixed via #366

@arp242 arp242 closed this as completed Oct 22, 2022
@joukewitteveen
Copy link

#366 does not appear to address the fact that isEmpty is used on fully dereferenced structs, while pointer types should only be considered empty when they are nil. Shall I open a new issue for that?

@arp242
Copy link
Collaborator

arp242 commented Oct 23, 2022

Shall I open a new issue for that?

Yes please @joukewitteveen; sorry, I didn't really register your comment back in August as I was busy with other things, and when I fixed this yesterday I just didn't see your comment so I didn't look in to it. It seems mostly unrelated anyway (other than being about the omitempty struct tag); usually best to err on the side of creating new issues for things, especially smaller projects like this where people only work on every once in a while. Just so easy to miss stuff otherwise.

bcl added a commit to bcl/osbuild-composer that referenced this issue Oct 26, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
thozza pushed a commit to bcl/osbuild-composer that referenced this issue Oct 27, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
bcl added a commit to bcl/osbuild-composer that referenced this issue Oct 27, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
bcl added a commit to bcl/osbuild-composer that referenced this issue Oct 27, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
gicmo pushed a commit to osbuild/osbuild-composer that referenced this issue Oct 31, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
natalieparellano added a commit to buildpacks/lifecycle that referenced this issue Nov 9, 2022
- BurntSushi/toml#360 is closed in 1.2.1

Signed-off-by: Natalie Arellano <narellano@vmware.com>
say-paul pushed a commit to say-paul/osbuild-composer that referenced this issue Nov 10, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.
bcl added a commit to bcl/osbuild-composer that referenced this issue Dec 6, 2022
This fixes an error when processing blueprints with customizations.firewall
See BurntSushi/toml#360 for details on the
upstream bug.

Resolves: rhbz#2150920
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants