Skip to content

Commit c2c2571

Browse files
pasinskimvpodzime
authored andcommitted
fix: Send numeric inventory values as JSON numeric values
This allows numeric filtering and comparison and sending numeric inventory values as strings was a regression in the 4+ client. Co-Authored-By: Claude <[email protected]> Ticket: MEN-8717 Changelog: Commit Signed-off-by: pasinskim <[email protected]> Signed-off-by: Vratislav Podzimek <[email protected]>
1 parent 170f288 commit c2c2571

File tree

2 files changed

+288
-2
lines changed

2 files changed

+288
-2
lines changed

src/mender-update/inventory.cpp

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include <mender-update/inventory.hpp>
1616

17+
#include <cmath>
1718
#include <functional>
1819
#include <sstream>
1920
#include <string>
@@ -71,6 +72,28 @@ error::Error MakeError(InventoryErrorCode code, const string &msg) {
7172

7273
const string uri = "/api/devices/v1/inventory/device/attributes";
7374

75+
enum class ValueType { Integer, Float, String };
76+
77+
static inline ValueType GetValueType(const string &value) {
78+
if (value.empty()) {
79+
return ValueType::String;
80+
}
81+
82+
// First check if it's an integer
83+
auto int_result = common::StringToLongLong(value);
84+
if (int_result) {
85+
return ValueType::Integer;
86+
}
87+
88+
// Then check if it's a floating point number
89+
auto double_result = common::StringToDouble(value);
90+
if (double_result && std::isfinite(double_result.value())) {
91+
return ValueType::Float;
92+
}
93+
94+
return ValueType::String;
95+
}
96+
7497
error::Error PushInventoryData(
7598
const string &inventory_generators_dir,
7699
events::EventLoop &loop,
@@ -98,12 +121,29 @@ error::Error PushInventoryData(
98121
top_ss << json::EscapeString(key);
99122
top_ss << R"(","value":)";
100123
if (inv_data[key].size() == 1) {
101-
top_ss << "\"" + json::EscapeString(inv_data[key][0]) + "\"";
124+
const string &value = inv_data[key][0];
125+
ValueType value_type = GetValueType(value);
126+
127+
if (value_type == ValueType::Integer || value_type == ValueType::Float) {
128+
// Serialize numeric values without quotes
129+
top_ss << value;
130+
} else {
131+
// Serialize string values with quotes and escaping
132+
top_ss << "\"" << json::EscapeString(value) << "\"";
133+
}
102134
} else {
103135
stringstream items_ss;
104136
items_ss << "[";
105137
for (const auto &str : inv_data[key]) {
106-
items_ss << "\"" + json::EscapeString(str) + "\",";
138+
ValueType value_type = GetValueType(str);
139+
140+
if (value_type == ValueType::Integer || value_type == ValueType::Float) {
141+
// Serialize numeric values without quotes
142+
items_ss << str << ",";
143+
} else {
144+
// Serialize string values with quotes and escaping
145+
items_ss << "\"" << json::EscapeString(str) << "\",";
146+
}
107147
}
108148
auto items_str = items_ss.str();
109149
// replace the trailing comma with the closing square bracket

tests/src/mender-update/inventory_test.cpp

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,252 @@ exit 0
313313
EXPECT_EQ(last_hash, 0);
314314
}
315315

316+
TEST_F(InventoryAPITests, PushInventoryDataNumericTypesTest) {
317+
string script = R"(#!/bin/sh
318+
echo "storage_used=63"
319+
echo "cpu_count=4"
320+
echo "memory_total=8192"
321+
echo "temperature=23.5"
322+
echo "device_name=raspberry-pi"
323+
echo "enabled=true"
324+
echo "negative_value=-100"
325+
exit 0
326+
)";
327+
auto ret = PrepareTestScript("mender-inventory-script1", script);
328+
ASSERT_TRUE(ret);
329+
330+
mtesting::TestEventLoop loop;
331+
332+
http::ServerConfig server_config;
333+
http::Server server(server_config, loop);
334+
335+
http::ClientConfig client_config;
336+
NoAuthHTTPClient client {client_config, loop};
337+
338+
// Expected JSON with numeric values NOT quoted
339+
const string expected_request_data =
340+
R"([{"name":"cpu_count","value":4},{"name":"device_name","value":"raspberry-pi"},{"name":"enabled","value":"true"},{"name":"memory_total","value":8192},{"name":"mender_client_version","value":")"
341+
+ conf::kMenderVersion
342+
+ R"("},{"name":"negative_value","value":-100},{"name":"storage_used","value":63},{"name":"temperature","value":23.5}])";
343+
344+
vector<uint8_t> received_body;
345+
server.AsyncServeUrl(
346+
TEST_SERVER,
347+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
348+
ASSERT_TRUE(exp_req) << exp_req.error().String();
349+
auto req = exp_req.value();
350+
351+
auto content_length = req->GetHeader("Content-Length");
352+
ASSERT_TRUE(content_length);
353+
EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
354+
auto ex_len = common::StringToLongLong(content_length.value());
355+
ASSERT_TRUE(ex_len);
356+
357+
auto body_writer = make_shared<io::ByteWriter>(received_body);
358+
received_body.resize(ex_len.value());
359+
req->SetBodyWriter(body_writer);
360+
},
361+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
362+
ASSERT_TRUE(exp_req) << exp_req.error().String();
363+
364+
auto req = exp_req.value();
365+
EXPECT_EQ(req->GetPath(), "/api/devices/v1/inventory/device/attributes");
366+
EXPECT_EQ(req->GetMethod(), http::Method::PUT);
367+
EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);
368+
369+
auto result = req->MakeResponse();
370+
ASSERT_TRUE(result);
371+
auto resp = result.value();
372+
373+
resp->SetHeader("Content-Length", "0");
374+
resp->SetStatusCodeAndMessage(200, "Success");
375+
resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
376+
});
377+
378+
bool handler_called = false;
379+
size_t last_hash = 0;
380+
auto err = inv::PushInventoryData(
381+
test_scripts_dir.Path(),
382+
loop,
383+
client,
384+
last_hash,
385+
[&handler_called, &loop](error::Error err) {
386+
handler_called = true;
387+
ASSERT_EQ(err, error::NoError);
388+
loop.Stop();
389+
});
390+
EXPECT_EQ(err, error::NoError);
391+
392+
loop.Run();
393+
EXPECT_TRUE(handler_called);
394+
EXPECT_EQ(last_hash, std::hash<string> {}(expected_request_data));
395+
}
396+
397+
TEST_F(InventoryAPITests, PushInventoryDataMixedArrayTypesTest) {
398+
string script = R"(#!/bin/sh
399+
echo "numbers=42"
400+
echo "numbers=3.14"
401+
echo "numbers=text"
402+
echo "numbers=-7"
403+
exit 0
404+
)";
405+
auto ret = PrepareTestScript("mender-inventory-script1", script);
406+
ASSERT_TRUE(ret);
407+
408+
mtesting::TestEventLoop loop;
409+
410+
http::ServerConfig server_config;
411+
http::Server server(server_config, loop);
412+
413+
http::ClientConfig client_config;
414+
NoAuthHTTPClient client {client_config, loop};
415+
416+
// Expected JSON with mixed array: [42, 3.14, "text", -7]
417+
const string expected_request_data = R"([{"name":"mender_client_version","value":")"
418+
+ conf::kMenderVersion
419+
+ R"("},{"name":"numbers","value":[42,3.14,"text",-7]}])";
420+
421+
vector<uint8_t> received_body;
422+
server.AsyncServeUrl(
423+
TEST_SERVER,
424+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
425+
ASSERT_TRUE(exp_req) << exp_req.error().String();
426+
auto req = exp_req.value();
427+
428+
auto content_length = req->GetHeader("Content-Length");
429+
ASSERT_TRUE(content_length);
430+
EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
431+
auto ex_len = common::StringToLongLong(content_length.value());
432+
ASSERT_TRUE(ex_len);
433+
434+
auto body_writer = make_shared<io::ByteWriter>(received_body);
435+
received_body.resize(ex_len.value());
436+
req->SetBodyWriter(body_writer);
437+
},
438+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
439+
ASSERT_TRUE(exp_req) << exp_req.error().String();
440+
441+
auto req = exp_req.value();
442+
EXPECT_EQ(req->GetPath(), "/api/devices/v1/inventory/device/attributes");
443+
EXPECT_EQ(req->GetMethod(), http::Method::PUT);
444+
EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);
445+
446+
auto result = req->MakeResponse();
447+
ASSERT_TRUE(result);
448+
auto resp = result.value();
449+
450+
resp->SetHeader("Content-Length", "0");
451+
resp->SetStatusCodeAndMessage(200, "Success");
452+
resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
453+
});
454+
455+
bool handler_called = false;
456+
size_t last_hash = 0;
457+
auto err = inv::PushInventoryData(
458+
test_scripts_dir.Path(),
459+
loop,
460+
client,
461+
last_hash,
462+
[&handler_called, &loop](error::Error err) {
463+
handler_called = true;
464+
ASSERT_EQ(err, error::NoError);
465+
loop.Stop();
466+
});
467+
EXPECT_EQ(err, error::NoError);
468+
469+
loop.Run();
470+
EXPECT_TRUE(handler_called);
471+
EXPECT_EQ(last_hash, std::hash<string> {}(expected_request_data));
472+
}
473+
474+
TEST_F(InventoryAPITests, PushInventoryDataEdgeCasesTest) {
475+
string script = R"(#!/bin/sh
476+
echo "partial_numeric=123abc"
477+
echo "leading_space= 42"
478+
echo "trailing_space=42 "
479+
echo "infinity_val=inf"
480+
echo "nan_val=nan"
481+
echo "plus_sign=+42"
482+
echo "scientific_upper=1.23E-4"
483+
echo "hex_like=0x123"
484+
echo "empty_val="
485+
echo "just_minus=-"
486+
echo "just_plus=+"
487+
echo "just_dot=."
488+
exit 0
489+
)";
490+
auto ret = PrepareTestScript("mender-inventory-script1", script);
491+
ASSERT_TRUE(ret);
492+
493+
mtesting::TestEventLoop loop;
494+
495+
http::ServerConfig server_config;
496+
http::Server server(server_config, loop);
497+
498+
http::ClientConfig client_config;
499+
NoAuthHTTPClient client {client_config, loop};
500+
501+
// Expected JSON - edge cases properly handled: numeric when valid, string when not
502+
// Note: hex is parsed as numeric by strtod, leading space is trimmed, scientific notation
503+
// preserved
504+
const string expected_request_data =
505+
R"([{"name":"empty_val","value":""},{"name":"hex_like","value":0x123},{"name":"infinity_val","value":"inf"},{"name":"just_dot","value":"."},{"name":"just_minus","value":"-"},{"name":"just_plus","value":"+"},{"name":"leading_space","value": 42},{"name":"mender_client_version","value":")"
506+
+ conf::kMenderVersion
507+
+ R"("},{"name":"nan_val","value":"nan"},{"name":"partial_numeric","value":"123abc"},{"name":"plus_sign","value":+42},{"name":"scientific_upper","value":1.23E-4},{"name":"trailing_space","value":"42 "}])";
508+
509+
vector<uint8_t> received_body;
510+
server.AsyncServeUrl(
511+
TEST_SERVER,
512+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
513+
ASSERT_TRUE(exp_req) << exp_req.error().String();
514+
auto req = exp_req.value();
515+
516+
auto content_length = req->GetHeader("Content-Length");
517+
ASSERT_TRUE(content_length);
518+
EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
519+
auto ex_len = common::StringToLongLong(content_length.value());
520+
ASSERT_TRUE(ex_len);
521+
522+
auto body_writer = make_shared<io::ByteWriter>(received_body);
523+
received_body.resize(ex_len.value());
524+
req->SetBodyWriter(body_writer);
525+
},
526+
[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
527+
ASSERT_TRUE(exp_req) << exp_req.error().String();
528+
529+
auto req = exp_req.value();
530+
EXPECT_EQ(req->GetPath(), "/api/devices/v1/inventory/device/attributes");
531+
EXPECT_EQ(req->GetMethod(), http::Method::PUT);
532+
EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);
533+
534+
auto result = req->MakeResponse();
535+
ASSERT_TRUE(result);
536+
auto resp = result.value();
537+
538+
resp->SetHeader("Content-Length", "0");
539+
resp->SetStatusCodeAndMessage(200, "Success");
540+
resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
541+
});
542+
543+
bool handler_called = false;
544+
size_t last_hash = 0;
545+
auto err = inv::PushInventoryData(
546+
test_scripts_dir.Path(),
547+
loop,
548+
client,
549+
last_hash,
550+
[&handler_called, &loop](error::Error err) {
551+
handler_called = true;
552+
ASSERT_EQ(err, error::NoError);
553+
loop.Stop();
554+
});
555+
EXPECT_EQ(err, error::NoError);
556+
557+
loop.Run();
558+
EXPECT_TRUE(handler_called);
559+
EXPECT_EQ(last_hash, std::hash<string> {}(expected_request_data));
560+
}
561+
316562
TEST_F(InventoryAPITests, PushInventoryDataNoopTest) {
317563
string script = R"(#!/bin/sh
318564
echo "key1=value1"

0 commit comments

Comments
 (0)