lrstanley / girc

:bomb: girc is a flexible IRC library for Go :ok_hand:
https://pkg.go.dev/github.com/lrstanley/girc
MIT License
138 stars 13 forks source link

Use of struct{} instead of string where value is ignored #18

Closed bmeh closed 5 years ago

bmeh commented 5 years ago

I have stumbled upon the code here, where you are using string (as in map[string][]string) and nil, instead of struct{} and struct{}{}. From a quick glance I cannot see you using the value anywhere, and using struct{} would communicate this intent better, therefore making the codebase slightly cleaner, and additionally it would consume less memory, but this memory consumption is negligible either way.

From:

var possibleCap = map[string][]string{
  "account-notify": nil,
[...]
}

to:

var possibleCap = map[string]struct{}{
  "account-notify": struct{}{},
[...]
}

You may also like to compare the assembly output: https://go.godbolt.org/z/4IMjlE

lrstanley commented 5 years ago

the values are used here, thus changing to struct{}{} would break this: https://github.com/lrstanley/girc/blob/102f17f86306c2152a8c6188f9bb8b0e7288de31/cap.go#L103-L123

bmeh commented 5 years ago

This is actually odd, I changed everything to struct{} and struct{}{} and modified the snippet you posted, albeit I left parseCap() alone. It compiled and the tests were successful. I even used it to connect to a server through SASL (PLAIN), and it says it has the proper capabilities, but it says it doesn't have capability sasl through c.HasCapability, nor with mine or the original version. Yet the test cap_test (where you use possibleCapList["sasl"] instead though) passes, both with your version and mine.

Although I figured the reason you use []string is that a capability can be in the form of sasl=PLAIN,EXTERNAL, and you need to store both PLAIN, and EXTERNAL, but then why does all tests pass, and seemingly have identical results after I make those modifications? Everything seems to work correctly.

Loosely-related: is it possible right now to get PLAIN and EXTERNAL? Is it exposed? c.HasCapability returns only a bool, but what if I want to know the arguments of said capabilities? Should c.HasCapability return (bool, []string) perhaps? Worth noting again that c.HasCapability("sasl") returns false, so we probably wouldn't be able to get PLAIN and EXTERNAL if it doesn't even find the capability sasl. Maybe this part needs a complete rewrite or something. I might get into it later, unless you decide to do so, but I have to finish something first that relies on this library, and I don't need to check for the capability sasl. :P

lrstanley commented 5 years ago

c.HasCapability("sasl") works for me without issue, both on servers that don't provide any params (so it's assumed to be PLAIN), and servers that provide PLAIN,EXTERNAL. Did you verify the SASL connection was a success? Can you provide the server you used to test this on? This is what I used (can also test with testnet.oragono.io).

package main

import (
    "log"
    "os"

    "github.com/nmeum/girc"
)

func main() {
    client := girc.New(girc.Config{
        Server: "irc.byteirc.org",
        Port:   6667,
        Nick:   "girc-test1",
        User:   "girc",
        Debug:  os.Stdout,
        SASL:   &girc.SASLPlain{User: "*truncated*", Pass: "*truncated*"},
    })

    client.Handlers.AddBg(girc.CONNECTED, func(c *girc.Client, e girc.Event) {
        log.Printlny(c.HasCapability("sasl"))
    })

    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }
}

As for HasCapability, this is not the capabilities of the server, rather the capabilities that were successful between the client and server. Unfortunately, the IRCv3 spec does not specify the successful params back in the ACK response if the client happens to acknowledge one of the sasl mechanisms. So, the only way that HasCapability could return the specific ones that were acknowledged, is if the writing method appends that. That would have to be manually implemented for each IRCv3 extension which has parameters (which isn't really worth the effort or complexity), and would also break if the user decides to acknowledge things outside of the internal sasl ack in girc.

lrstanley commented 5 years ago

As for replacing []string with struct{}/struct{}{}/etc, it's a non-issue. The memory consumption doesn't matter since it's only used once maybe twice, in a very small scope. In addition, changing it would make the code more complex (we have to compare the possibleCap list against the SupportedCaps, why spend the time to try and differentiate struct{} vs []string when you can just compare the two). Lastly, this method supports future extensions of the IRCv3 spec, as future extensions, we may only support specific params, and this will take advantage of that. It's not really used right now, as only one entry of the spec really uses params.

bmeh commented 5 years ago

Ah, I just added:

if c.HasCapability("sasl") {
  log.Println("Has capability 'sasl'")
} else {
  log.Println("Does not have capability 'sasl'")
}

to my own code and it seems to work, so I am not entirely sure what the issue could have been. I did indeed check for RPL_SASLSUCCESS, and despite that, c.HasCapability("sasl") didn't work. It might have been my fault though, I have several different versions scattered around my filesystem, so it's possible that I was running a version without SASL enabled (but then why did I get SASL authentication succeeded?). Sorry about that. Sleep deprivation at its finest. :D

lrstanley commented 5 years ago

checking right after RPL_SASLSUCCESS may be a timing issue, since the client doesn't consider SASL fully successful until the CAP transaction is complete and shows SASL in the ACK list iirc (on my phone atm). I usually wait for the custom CONNECTED event, as generally at that point the handshaking and other connection related state changes are complete.

---- On Wed, 05 Sep 2018 22:14:05 -0500 notifications@github.com wrote ----

Ah, I just added:

if c.HasCapability("sasl") { log.Println("Has capability 'sasl'") } else { log.Println("Does not have capability 'sasl'") } to my own code and it seems to work, so I am not entirely sure what the issue could have been. I did indeed check for RPL_SASLSUCCESS, and despite that, c.HasCapability("sasl") didn't work. It might have been my fault though, I have several different versions scattered around my filesystem, so it's possible that I was running a version without SASL enabled. Sorry about that. Sleep deprivation at its finest. :D

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub, or mute the thread.

bmeh commented 5 years ago

What I meant by checking RPL_SASLSUCCESS is that I added a handler for it which prints a message that it succeeded, and then I did an INVITE (poor man's test case) afterwards which calls c.HasCapability("sasl"), and it failed. I cannot reproduce this now anymore, so I am not exactly sure what caused this issue. It all seems to work just fine now using my latest code. I will give a detailed report and dig deeper if I run into this issue.

It might have been that I used my friend's server who ran the same version of oragono, but with a different configuration. I remember not being able to type NICK <account> in case the nick was registered (hence "account"), which was odd because according to oragono's documentations, the account name has nothing to do with the nick. Sadly I cannot reproduce this either, I will have to ask my friend to set his server up which: 1) doesn't append nicknames in responses, causing more off-by-one issues, 2) requires SASL before sending the raw command NICK <name> where name is an account. It works fine with my configuration, so I really have to ask him for his. :D

Edit: I did mention some more details in the other opened issue (https://github.com/lrstanley/girc/issues/19?ts=2#issuecomment-419084095) regarding those peculiar behaviors.