Re: [PATCH v5] Add JSON output options to 'iw' for scan results

Johannes Berg
2024-03-05T09:41:29Z

On Mon, 2024-03-04 at 23:01 +0300, Isaev Ruslan wrote:
> v5 changes:
> - add json print to print_ap_channel_report()
> - minor refactor open_json_object()

Alright, yay, so mechanical submission issues out of the way, this looks
readable :)

> branch: v5.19

That's almost two years old, so you'll obviously have to rebase it on
the latest main branch. But we can have more fundamental discussions
first.

> This commit introduces the ability to output scan results from 'iw' in
> JSON format, similar to the 'ip' utility from the iproute2 package.
> The addition aims to enhance the tool's usability and scriptability
> by providing a structured data format option.

Generally, I'm not opposed to that, but I think we've had this
discussion before, and I don't want to see a lot of extra complexity in
the code for it.

> Two new command-line options are added:
> - '-j': Outputs scan results in compact JSON format.
> - '-jj': Outputs scan results in pretty-printed JSON format.

With tools like 'jq' being near ubiquitous, is there much value in
having two output formats? I'd almost say only support -j (and I don't
like '-jj' anyway, probably would prefer -j/--json and --json=pretty or
so), and then 'jq' can do pretty-printing and coloring etc.?

> + //close root json object and deinit jsonw
> + if(iw_json){
> + delete_json_obj();
> + }

Please generally try to follow the (existing/Linux) coding style more
closely. I don't mind C99 comments but a space would be nice, space
after if, space before the opening brace, no braces needed here though,
perhaps more (just not on these four lines :-) )

> +++ b/json/json_print.c

I'm generally not going to look into these files for now, but including
them internally means we have to maintain them. I'd almost prefer a
library that can be used.

However, with that said,

> +/*
> + * json_print.c "print regular or json output, based on json_writer".
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version
> + * 2 of the License, or (at your option) any later version.

This doesn't work well with the ISC license of iw. Which is another
reason to prefer an existing library, I suppose.

> +++ b/json/json_writer.c
> @@ -0,0 +1,296 @@
> +// SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) //

We don't use SPDX license tags here yet, so not sure this is meaningful,
we also don't have a dir with the actual license texts like the kernel
has for instance.

If going this way it'd probably be useful to add SPDX tags to all
existing files too.

Here, off the top of my head, I suspect the BSD-2-Clause should actually
be compatible to releasing the whole thing under ISC, but need to check
that too.

> --- a/scan.c
> +++ b/scan.c
> @@ -12,6 +12,8 @@
> #include "nl80211.h"
> #include "iw.h"
>
> +#include "json/json_print.h"
> +
> #define WLAN_CAPABILITY_ESS (1<<0)
> #define WLAN_CAPABILITY_IBSS (1<<1)
> #define WLAN_CAPABILITY_CF_POLLABLE (1<<2)
> @@ -546,10 +548,13 @@ static int handle_scan(struct nl80211_state *state,
>
> static void tab_on_first(bool *first)
> {
> - if (!*first)
> - printf("\t");
> - else
> + if (!*first){
> + if(!iw_json){
> + printf("\t");
> + }
> + }else{
> *first = false;
> + }

Coding style. Not sure why you don't just skip the whole thing if json
is turned on?

> static void print_ssid(const uint8_t type, uint8_t len, const uint8_t *data,
> const struct print_ies_data *ie_buffer)
> {
> - printf(" ");
> - print_ssid_escaped(len, data);
> - printf("\n");
> + if(!iw_json){
> + printf(" ");
> + print_ssid_escaped(len, data);
> + printf("\n");
> + } else {
> + print_ssid_escaped(len, data);
> + }
> }

Might be more useful to provide a JSON-output for SSIDs? Surely you'd
also want to escape them in a JSON-compatible way, since you'd want to
use '\uXXXX' encoding to actually get proper bytes out on the other side
without a special decoder?

> @@ -574,21 +583,35 @@ static void print_supprates(const uint8_t type, uint8_t len,
> {
> int i;
>
> - printf(" ");
> -
> - for (i = 0; i < len; i++) {
> - int r = data[i] & 0x7f;
> + if(iw_json){
> + open_json_array("array", NULL);
> + for (i = 0; i < len; i++) {
> + int r = data[i] & 0x7f;
> + if (r == BSS_MEMBERSHIP_SELECTOR_VHT_PHY && data[i] & 0x80){
> + print_string(PRINT_JSON, NULL, "VHT%s", data[i] & 0x80 ? "*" : "");
> + } else if (r == BSS_MEMBERSHIP_SELECTOR_HT_PHY && data[i] & 0x80){
> + print_string(PRINT_JSON, NULL, "HT%s", data[i] & 0x80 ? "*" : "");
> + } else {
> + print_string(PRINT_JSON, NULL, "%d.%d%s", r/2, 5*(r&1), data[i] & 0x80 ? "*" : "");
> + }
> + }
> + close_json_array(NULL);
> + }else{
> + printf(" ");
> + for (i = 0; i < len; i++) {
> + int r = data[i] & 0x7f;

So generally, here, no - I don't like this style of working at all. We'd
really want to have as little "if (iw_json)" as possible in the code. In
code like this, we really shouldn't have such an if at all, but only
e.g.

open_json_array();
for (...) {
if (...)
print_string(...);
}
close_json_array();

Surely we can come up with an abstraction where something like
print_string() does the right thing either way? And open/close JSON
array does the '\n' needed after it, or something?
Maybe we'd pass an argument to open_json_array() that says how we want
it printed in the non-JSON case (and rename it to open_array?), e.g.
should it all be on one line like here, or should it be multiple lines
with " - " prefix on each line, etc. JSON also tracks nesting, I guess,
so we could even do that for nested stuff in the normal output where we
use tabs etc.?

Generally, I'd also say that we never promised stable output, so if the
output changes slightly due to such, I'm perfectly fine with that,
whoever relied on it (and that includes me in some internal code) gets
to keep the pieces when it breaks ...

Also, looking at the function calls, all those NULL and PRINT_JSON are
pointless, as far as I can tell? So I think it'd make more sense to come
up with a JSON-supporting but otherwise agnostic API abstraction:

open_array(SINGLE_LINE); // could also be MULTI_LINE
for ...
print_string("VHT%s", data[i] & 0x80 ? "*" : "");
close_array();

Maybe open_array returns some kind of token that you pass to the inner
calls down to close_array(), or maybe it just keeps a stack of these
internally, I could see either way working (though an internal stack
would probably be simpler to use, and with a lot of code using it, that
might make sense for to trade off against the additional internal
complexity)

Somewhat unrelated to that discussion, you might also want to not
actually use print_string() for rates here though, and possibly use a
different JSON encoding for the rates (and membership selectors), e.g.
as an object with a 'basic' property? But I don't care much about that
part, though generally I'd argue strings shouldn't be used where there's
a better representation with native JSON types.

> + if(iw_json){
> + print_string(PRINT_JSON, "capabilities_raw", "0x%02x 0x%02x 0x%02x 0x%02x 0x%02x", data[0], data[1], data[2], data[3], data[4]);

That's a really bad JSON representation, a string with bytes, rather
than an array of bytes?

> static void print_erp(const uint8_t type, uint8_t len, const uint8_t *data,
> const struct print_ies_data *ie_buffer)
> {
> if (data[0] == 0x00)
> - printf(" <no flags>");
> + iw_json ? print_bool(PRINT_JSON, "no_flags", "%s", true) : printf(" <no flags>");

It seems like you'd generally want to have a container for each of
these, so you have

// probably these two lines outside of the print_erp() call even
open_json_object("erp");
print_u8("tag", type);

print_flags(...);

or something, rather than including "no_flags" in the object?

> + iw_json ? print_string(PRINT_JSON, NULL, "WEP-40") : printf("WEP-40");

This goes just along with what I wrote above, but that iw_json thing
here seems really pointless, and just makes things difficult.

(I also don't like the use of the ternary operator in this way but
that's maybe not quite the point.)

[snip lots of similar stuff]

> unsigned int bw160[] = { 5180, 5500, 5955, 6115, 6275, 6435,
> - 6595, 6755, 6915 };
> + 6595, 6755, 6915 };

nit: unrelated whitespace change

So ... Like I wrote above, generally I'm not against doing this. But
like I also tried to explain above, I think it needs to be less
"duplicative". I'm happy to change the code in major ways to make JSON
output easier, I'm also happy to let that change the output in some ways
(maybe the default should be YAML-compatible ;-) ).

But I'd really want to see this done in a way that doesn't end up with
having to duplicate everything all the time.

We'll also need to figure out the licensing situation, and perhaps
ideally find a way to not add ~1.5k LOC to support it, but link against
something that exists already.

johannes