File indexing completed on 2024-05-26 04:57:38

0001 /**
0002  * \file jsoncliformatter.cpp
0003  * CLI formatter with JSON input and output.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 28 Jul 2019
0008  *
0009  * Copyright (C) 2019-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "jsoncliformatter.h"
0028 #include <climits>
0029 #include <QJsonArray>
0030 #include <QJsonDocument>
0031 #include <QVariantMap>
0032 #include <QStringBuilder>
0033 #include "clierror.h"
0034 #include "abstractcli.h"
0035 #include "frame.h"
0036 
0037 namespace {
0038 
0039 int jsonRpcErrorCode(CliError errorCode)
0040 {
0041   // See error codes at https://www.jsonrpc.org/specification
0042   int code = -1;
0043   switch (errorCode) {
0044   case CliError::Ok:
0045     code = 0;
0046     break;
0047   case CliError::ApplicationError:
0048     code = -1;
0049     break;
0050   case CliError::ParseError:
0051     code = -32700;
0052     break;
0053   case CliError::InvalidRequest:
0054   case CliError::Usage:
0055     code = -32600;
0056     break;
0057   case CliError::MethodNotFound:
0058     code = -32601;
0059     break;
0060   case CliError::InvalidParams:
0061     code = -32602;
0062     break;
0063   case CliError::InternalError:
0064     code = -32603;
0065     break;
0066   }
0067   return code;
0068 }
0069 
0070 }
0071 
0072 
0073 JsonCliFormatter::JsonCliFormatter(AbstractCliIO* io)
0074   : AbstractCliFormatter(io), m_compact(false)
0075 {
0076 }
0077 
0078 JsonCliFormatter::~JsonCliFormatter()
0079 {
0080 }
0081 
0082 void JsonCliFormatter::clear()
0083 {
0084   m_jsonRequest.clear();
0085   m_jsonId.clear();
0086   m_errorMessage.clear();
0087   m_args.clear();
0088   m_response = QJsonObject();
0089   m_compact = false;
0090 }
0091 
0092 QStringList JsonCliFormatter::parseArguments(const QString& line)
0093 {
0094   m_errorMessage.clear();
0095   m_args.clear();
0096   if (m_jsonRequest.isEmpty()) {
0097     m_jsonRequest = line.trimmed();
0098     if (!m_jsonRequest.startsWith(QLatin1Char('{'))) {
0099       m_jsonRequest.clear();
0100     }
0101   } else {
0102     m_jsonRequest.append(line.trimmed());
0103   }
0104   if (!m_jsonRequest.isEmpty()) {
0105     if (!m_jsonRequest.endsWith(QLatin1Char('}'))) {
0106       // Probably partial JSON request
0107       return QStringList();
0108     }
0109     m_compact = m_jsonRequest.contains(QLatin1String("\"method\":\""));
0110     QJsonParseError error;
0111     if (auto doc = QJsonDocument::fromJson(m_jsonRequest.toUtf8(), &error);
0112         !doc.isNull()) {
0113       if (QJsonObject obj = doc.object(); !obj.isEmpty()) {
0114         if (auto method = obj.value(QLatin1String("method")).toString();
0115             !method.isEmpty()) {
0116           m_args.append(method);
0117           const auto params = obj.value(QLatin1String("params")).toArray();
0118           for (const auto& param : params) {
0119             QString arg = param.toString();
0120             if (arg.isEmpty()) {
0121               if (param.isArray()) {
0122                 // Special handling for tags parameter of the form [1, 2]
0123                 const auto elements = param.toArray();
0124                 for (const auto& element : elements) {
0125                   if (int tagNr = element.toInt();
0126                       tagNr > 0 && tagNr <= Frame::Tag_NumValues) {
0127                     arg += QLatin1Char('0' + static_cast<char>(tagNr));
0128                   } else {
0129                     arg.clear();
0130                     break;
0131                   }
0132                 }
0133               } else if (param.isDouble()) {
0134                 // Allow integer numbers, for example for track numbers
0135                 if (int argInt = param.toInt(INT_MIN); argInt != INT_MIN) {
0136                   arg = QString::number(argInt);
0137                 }
0138               } else if (param.isBool()) {
0139                 arg = QLatin1String(param.toBool() ? "true" : "false");
0140               }
0141             }
0142             m_args.append(arg);
0143           }
0144           // A JSON-RPC ID is used in the response and to store that a JSON
0145           // request is running.
0146           m_jsonId = obj.value(QLatin1String("id"))
0147               .toString(QLatin1String(""));
0148         }
0149       }
0150     }
0151     if (m_args.isEmpty()) {
0152       if (auto errStr = error.error != QJsonParseError::NoError
0153               ? error.errorString() : QLatin1String("missing method");
0154           !errStr.isEmpty()) {
0155         m_errorMessage = errStr + QLatin1String(": ") + m_jsonRequest;
0156       }
0157       m_jsonRequest.clear();
0158       return QStringList();
0159     }
0160     m_jsonRequest.clear();
0161   } else {
0162     m_jsonId.clear();
0163   }
0164   return m_args;
0165 }
0166 
0167 QString JsonCliFormatter::getErrorMessage() const
0168 {
0169   return m_errorMessage;
0170 }
0171 
0172 bool JsonCliFormatter::isIncomplete() const
0173 {
0174   return !m_jsonRequest.isEmpty();
0175 }
0176 
0177 bool JsonCliFormatter::isFormatRecognized() const
0178 {
0179   return !m_jsonId.isNull() || !m_jsonRequest.isEmpty() ||
0180       !m_errorMessage.isEmpty();
0181 }
0182 
0183 void JsonCliFormatter::writeError(CliError errorCode)
0184 {
0185   QString msg;
0186   if (errorCode == CliError::MethodNotFound) {
0187 #if QT_VERSION >= 0x050600
0188     msg = tr("Unknown command '%1'")
0189         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.constFirst());
0190 #else
0191     msg = tr("Unknown command '%1'")
0192         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.first());
0193 #endif
0194   }
0195   writeErrorMessage(msg, jsonRpcErrorCode(errorCode));
0196 }
0197 
0198 void JsonCliFormatter::writeError(const QString& msg)
0199 {
0200   writeErrorMessage(msg, -1);
0201 }
0202 
0203 void JsonCliFormatter::writeError(const QString& msg, CliError errorCode)
0204 {
0205   QString errorMsg = msg;
0206   if (errorCode == CliError::Usage) {
0207     errorMsg = tr("Usage:") % QLatin1Char(' ') % errorMsg;
0208   }
0209   writeErrorMessage(errorMsg, jsonRpcErrorCode(errorCode));
0210 }
0211 
0212 void JsonCliFormatter::writeErrorMessage(const QString& msg, int code)
0213 {
0214   QJsonObject error;
0215   error.insert(QLatin1String("code"), code);
0216   error.insert(QLatin1String("message"), msg);
0217   m_response.insert(QLatin1String("error"), error);
0218 }
0219 
0220 void JsonCliFormatter::writeResult(const QString& str)
0221 {
0222   m_response.insert(QLatin1String("result"), str);
0223 }
0224 
0225 void JsonCliFormatter::writeResult(const QStringList& strs)
0226 {
0227   m_response.insert(QLatin1String("result"), QJsonArray::fromStringList(strs));
0228 }
0229 
0230 void JsonCliFormatter::writeResult(const QVariantMap& map)
0231 {
0232   QJsonObject result;
0233   if (map.size() == 1 && map.contains(QLatin1String("event"))) {
0234     result = m_response.value(QLatin1String("result")).toObject();
0235     auto events = result.value(QLatin1String("events")).toArray();
0236     events.append(QJsonValue::fromVariant(map.value(QLatin1String("event"))));
0237     result.insert(QLatin1String("events"), events);
0238   } else {
0239     result = QJsonObject::fromVariantMap(map);
0240   }
0241   m_response.insert(QLatin1String("result"), result);
0242 }
0243 
0244 void JsonCliFormatter::writeResult(bool result)
0245 {
0246   m_response.insert(QLatin1String("result"), result);
0247 }
0248 
0249 void JsonCliFormatter::finishWriting()
0250 {
0251   if (m_response.isEmpty()) {
0252     m_response.insert(QLatin1String("result"), QJsonValue::Null);
0253   }
0254   if (!m_jsonId.isEmpty()) {
0255     m_response.insert(QLatin1String("jsonrpc"), QLatin1String("2.0"));
0256     m_response.insert(QLatin1String("id"), m_jsonId);
0257   }
0258   io()->writeLine(QString::fromUtf8(
0259                     QJsonDocument(m_response).toJson(
0260                       m_compact ? QJsonDocument::Compact
0261                                 : QJsonDocument::Indented)));
0262 }