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 }