1 /**
2  * \file jsoncliformatter.cpp
3  * CLI formatter with JSON input and output.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 28 Jul 2019
8  *
9  * Copyright (C) 2019  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "jsoncliformatter.h"
28 #include <climits>
29 #include <QJsonArray>
30 #include <QJsonDocument>
31 #include <QVariantMap>
32 #include <QStringBuilder>
33 #include "clierror.h"
34 #include "abstractcli.h"
35 #include "frame.h"
36 
37 namespace {
38 
jsonRpcErrorCode(CliError errorCode)39 int jsonRpcErrorCode(CliError errorCode)
40 {
41   // See error codes at https://www.jsonrpc.org/specification
42   int code = -1;
43   switch (errorCode) {
44   case CliError::Ok:
45     code = 0;
46     break;
47   case CliError::ApplicationError:
48     code = -1;
49     break;
50   case CliError::ParseError:
51     code = -32700;
52     break;
53   case CliError::InvalidRequest:
54   case CliError::Usage:
55     code = -32600;
56     break;
57   case CliError::MethodNotFound:
58     code = -32601;
59     break;
60   case CliError::InvalidParams:
61     code = -32602;
62     break;
63   case CliError::InternalError:
64     code = -32603;
65     break;
66   }
67   return code;
68 }
69 
70 }
71 
72 
JsonCliFormatter(AbstractCliIO * io)73 JsonCliFormatter::JsonCliFormatter(AbstractCliIO* io)
74   : AbstractCliFormatter(io), m_compact(false)
75 {
76 }
77 
~JsonCliFormatter()78 JsonCliFormatter::~JsonCliFormatter()
79 {
80 }
81 
clear()82 void JsonCliFormatter::clear()
83 {
84   m_jsonRequest.clear();
85   m_jsonId.clear();
86   m_errorMessage.clear();
87   m_args.clear();
88   m_response = QJsonObject();
89   m_compact = false;
90 }
91 
parseArguments(const QString & line)92 QStringList JsonCliFormatter::parseArguments(const QString& line)
93 {
94   m_errorMessage.clear();
95   m_args.clear();
96   if (m_jsonRequest.isEmpty()) {
97     m_jsonRequest = line.trimmed();
98     if (!m_jsonRequest.startsWith(QLatin1Char('{'))) {
99       m_jsonRequest.clear();
100     }
101   } else {
102     m_jsonRequest.append(line.trimmed());
103   }
104   if (!m_jsonRequest.isEmpty()) {
105     if (!m_jsonRequest.endsWith(QLatin1Char('}'))) {
106       // Probably partial JSON request
107       return QStringList();
108     }
109     m_compact = m_jsonRequest.contains(QLatin1String("\"method\":\""));
110     QJsonParseError error;
111     auto doc = QJsonDocument::fromJson(m_jsonRequest.toUtf8(), &error);
112     if (!doc.isNull()) {
113       QJsonObject obj = doc.object();
114       if (!obj.isEmpty()) {
115         auto method = obj.value(QLatin1String("method")).toString();
116         if (!method.isEmpty()) {
117           m_args.append(method);
118           const auto params = obj.value(QLatin1String("params")).toArray();
119           for (const auto& param : params) {
120             QString arg = param.toString();
121             if (arg.isEmpty()) {
122               if (param.isArray()) {
123                 // Special handling for tags parameter of the form [1, 2]
124                 const auto elements = param.toArray();
125                 for (const auto& element : elements) {
126                   int tagNr = element.toInt();
127                   if (tagNr > 0 && tagNr <= Frame::Tag_NumValues) {
128                     arg += QLatin1Char('0' + static_cast<char>(tagNr));
129                   } else {
130                     arg.clear();
131                     break;
132                   }
133                 }
134               } else if (param.isDouble()) {
135                 // Allow integer numbers, for example for track numbers
136                 int argInt = param.toInt(INT_MIN);
137                 if (argInt != INT_MIN) {
138                   arg = QString::number(argInt);
139                 }
140               } else if (param.isBool()) {
141                 arg = QLatin1String(param.toBool() ? "true" : "false");
142               }
143             }
144             m_args.append(arg);
145           }
146           // A JSON-RPC ID is used in the response and to store that a JSON
147           // request is running.
148           m_jsonId = obj.value(QLatin1String("id"))
149               .toString(QLatin1String(""));
150         }
151       }
152     }
153     if (m_args.isEmpty()) {
154       auto errStr = error.error != QJsonParseError::NoError
155           ? error.errorString() : QLatin1String("missing method");
156       if (!errStr.isEmpty()) {
157         m_errorMessage = errStr + QLatin1String(": ") + m_jsonRequest;
158       }
159       m_jsonRequest.clear();
160       return QStringList();
161     }
162     m_jsonRequest.clear();
163   } else {
164     m_jsonId.clear();
165   }
166   return m_args;
167 }
168 
getErrorMessage() const169 QString JsonCliFormatter::getErrorMessage() const
170 {
171   return m_errorMessage;
172 }
173 
isIncomplete() const174 bool JsonCliFormatter::isIncomplete() const
175 {
176   return !m_jsonRequest.isEmpty();
177 }
178 
isFormatRecognized() const179 bool JsonCliFormatter::isFormatRecognized() const
180 {
181   return !m_jsonId.isNull() || !m_jsonRequest.isEmpty() ||
182       !m_errorMessage.isEmpty();
183 }
184 
writeError(CliError errorCode)185 void JsonCliFormatter::writeError(CliError errorCode)
186 {
187   QString msg;
188   if (errorCode == CliError::MethodNotFound) {
189 #if QT_VERSION >= 0x050600
190     msg = tr("Unknown command '%1'")
191         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.constFirst());
192 #else
193     msg = tr("Unknown command '%1'")
194         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.first());
195 #endif
196   }
197   writeErrorMessage(msg, jsonRpcErrorCode(errorCode));
198 }
199 
writeError(const QString & msg)200 void JsonCliFormatter::writeError(const QString& msg)
201 {
202   writeErrorMessage(msg, -1);
203 }
204 
writeError(const QString & msg,CliError errorCode)205 void JsonCliFormatter::writeError(const QString& msg, CliError errorCode)
206 {
207   QString errorMsg = msg;
208   if (errorCode == CliError::Usage) {
209     errorMsg = tr("Usage:") % QLatin1Char(' ') % errorMsg;
210   }
211   writeErrorMessage(errorMsg, jsonRpcErrorCode(errorCode));
212 }
213 
writeErrorMessage(const QString & msg,int code)214 void JsonCliFormatter::writeErrorMessage(const QString& msg, int code)
215 {
216   QJsonObject error;
217   error.insert(QLatin1String("code"), code);
218   error.insert(QLatin1String("message"), msg);
219   m_response.insert(QLatin1String("error"), error);
220 }
221 
writeResult(const QString & str)222 void JsonCliFormatter::writeResult(const QString& str)
223 {
224   m_response.insert(QLatin1String("result"), str);
225 }
226 
writeResult(const QStringList & strs)227 void JsonCliFormatter::writeResult(const QStringList& strs)
228 {
229   m_response.insert(QLatin1String("result"), QJsonArray::fromStringList(strs));
230 }
231 
writeResult(const QVariantMap & map)232 void JsonCliFormatter::writeResult(const QVariantMap& map)
233 {
234   QJsonObject result;
235   if (map.size() == 1 && map.contains(QLatin1String("event"))) {
236     result = m_response.value(QLatin1String("result")).toObject();
237     auto events = result.value(QLatin1String("events")).toArray();
238     events.append(QJsonValue::fromVariant(map.value(QLatin1String("event"))));
239     result.insert(QLatin1String("events"), events);
240   } else {
241     result = QJsonObject::fromVariantMap(map);
242   }
243   m_response.insert(QLatin1String("result"), result);
244 }
245 
writeResult(bool result)246 void JsonCliFormatter::writeResult(bool result)
247 {
248   m_response.insert(QLatin1String("result"), result);
249 }
250 
finishWriting()251 void JsonCliFormatter::finishWriting()
252 {
253   if (m_response.isEmpty()) {
254     m_response.insert(QLatin1String("result"), QJsonValue::Null);
255   }
256   if (!m_jsonId.isEmpty()) {
257     m_response.insert(QLatin1String("jsonrpc"), QLatin1String("2.0"));
258     m_response.insert(QLatin1String("id"), m_jsonId);
259   }
260   io()->writeLine(QString::fromUtf8(
261                     QJsonDocument(m_response).toJson(
262                       m_compact ? QJsonDocument::Compact
263                                 : QJsonDocument::Indented)));
264 }
265