/* * Copyright (c) 2021, The OpenThread Authors. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * @file * This file contains implementation of the CLI output module. */ #include "cli_utils.hpp" #include #include #include #if OPENTHREAD_FTD || OPENTHREAD_MTD #include #endif #include #include "cli/cli.hpp" #include "common/string.hpp" namespace ot { namespace Cli { const char Utils::kUnknownString[] = "unknown"; OutputImplementer::OutputImplementer(otCliOutputCallback aCallback, void *aCallbackContext) : mCallback(aCallback) , mCallbackContext(aCallbackContext) #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE , mOutputLength(0) , mEmittingCommandOutput(true) #endif { } void Utils::OutputFormat(const char *aFormat, ...) { va_list args; va_start(args, aFormat); OutputFormatV(aFormat, args); va_end(args); } void Utils::OutputFormat(uint8_t aIndentSize, const char *aFormat, ...) { va_list args; OutputSpaces(aIndentSize); va_start(args, aFormat); OutputFormatV(aFormat, args); va_end(args); } void Utils::OutputLine(const char *aFormat, ...) { va_list args; va_start(args, aFormat); OutputFormatV(aFormat, args); va_end(args); OutputNewLine(); } void Utils::OutputLine(uint8_t aIndentSize, const char *aFormat, ...) { va_list args; OutputSpaces(aIndentSize); va_start(args, aFormat); OutputFormatV(aFormat, args); va_end(args); OutputNewLine(); } void Utils::OutputNewLine(void) { OutputFormat("\r\n"); } void Utils::OutputSpaces(uint8_t aCount) { OutputFormat("%*s", aCount, ""); } void Utils::OutputBytes(const uint8_t *aBytes, uint16_t aLength) { for (uint16_t i = 0; i < aLength; i++) { OutputFormat("%02x", aBytes[i]); } } void Utils::OutputBytesLine(const uint8_t *aBytes, uint16_t aLength) { OutputBytes(aBytes, aLength); OutputNewLine(); } const char *Utils::Uint64ToString(uint64_t aUint64, Uint64StringBuffer &aBuffer) { char *cur = &aBuffer.mChars[Uint64StringBuffer::kSize - 1]; *cur = '\0'; if (aUint64 == 0) { *(--cur) = '0'; } else { for (; aUint64 != 0; aUint64 /= 10) { *(--cur) = static_cast('0' + static_cast(aUint64 % 10)); } } return cur; } void Utils::OutputUint64(uint64_t aUint64) { Uint64StringBuffer buffer; OutputFormat("%s", Uint64ToString(aUint64, buffer)); } void Utils::OutputUint64Line(uint64_t aUint64) { OutputUint64(aUint64); OutputNewLine(); } void Utils::OutputEnabledDisabledStatus(bool aEnabled) { OutputLine(aEnabled ? "Enabled" : "Disabled"); } #if OPENTHREAD_FTD || OPENTHREAD_MTD void Utils::OutputIp6Address(const otIp6Address &aAddress) { char string[OT_IP6_ADDRESS_STRING_SIZE]; otIp6AddressToString(&aAddress, string, sizeof(string)); return OutputFormat("%s", string); } void Utils::OutputIp6AddressLine(const otIp6Address &aAddress) { OutputIp6Address(aAddress); OutputNewLine(); } void Utils::OutputIp6Prefix(const otIp6Prefix &aPrefix) { char string[OT_IP6_PREFIX_STRING_SIZE]; otIp6PrefixToString(&aPrefix, string, sizeof(string)); OutputFormat("%s", string); } void Utils::OutputIp6PrefixLine(const otIp6Prefix &aPrefix) { OutputIp6Prefix(aPrefix); OutputNewLine(); } void Utils::OutputIp6Prefix(const otIp6NetworkPrefix &aPrefix) { OutputFormat("%x:%x:%x:%x::/64", (aPrefix.m8[0] << 8) | aPrefix.m8[1], (aPrefix.m8[2] << 8) | aPrefix.m8[3], (aPrefix.m8[4] << 8) | aPrefix.m8[5], (aPrefix.m8[6] << 8) | aPrefix.m8[7]); } void Utils::OutputIp6PrefixLine(const otIp6NetworkPrefix &aPrefix) { OutputIp6Prefix(aPrefix); OutputNewLine(); } void Utils::OutputSockAddr(const otSockAddr &aSockAddr) { char string[OT_IP6_SOCK_ADDR_STRING_SIZE]; otIp6SockAddrToString(&aSockAddr, string, sizeof(string)); return OutputFormat("%s", string); } void Utils::OutputSockAddrLine(const otSockAddr &aSockAddr) { OutputSockAddr(aSockAddr); OutputNewLine(); } void Utils::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength) { otDnsTxtEntry entry; otDnsTxtEntryIterator iterator; bool isFirst = true; otDnsInitTxtEntryIterator(&iterator, aTxtData, aTxtDataLength); OutputFormat("["); while (otDnsGetNextTxtEntry(&iterator, &entry) == OT_ERROR_NONE) { if (!isFirst) { OutputFormat(", "); } if (entry.mKey == nullptr) { // A null `mKey` indicates that the key in the entry is // longer than the recommended max key length, so the entry // could not be parsed. In this case, the whole entry is // returned encoded in `mValue`. OutputFormat("["); OutputBytes(entry.mValue, entry.mValueLength); OutputFormat("]"); } else { OutputFormat("%s", entry.mKey); if (entry.mValue != nullptr) { OutputFormat("="); OutputBytes(entry.mValue, entry.mValueLength); } } isFirst = false; } OutputFormat("]"); } const char *Utils::PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer) { uint32_t scaledValue = aValue; StringWriter writer(aBuffer.mChars, sizeof(aBuffer.mChars)); scaledValue = (scaledValue * 10000) / 0xffff; writer.Append("%u.%02u", static_cast(scaledValue / 100), static_cast(scaledValue % 100)); return aBuffer.mChars; } #endif // OPENTHREAD_FTD || OPENTHREAD_MTD void Utils::OutputFormatV(const char *aFormat, va_list aArguments) { mImplementer.OutputV(aFormat, aArguments); } void OutputImplementer::OutputV(const char *aFormat, va_list aArguments) { #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE va_list args; int charsWritten; bool truncated = false; va_copy(args, aArguments); #endif mCallback(mCallbackContext, aFormat, aArguments); #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE VerifyOrExit(mEmittingCommandOutput); charsWritten = vsnprintf(&mOutputString[mOutputLength], sizeof(mOutputString) - mOutputLength, aFormat, args); VerifyOrExit(charsWritten >= 0, mOutputLength = 0); if (static_cast(charsWritten) >= sizeof(mOutputString) - mOutputLength) { truncated = true; mOutputLength = sizeof(mOutputString) - 1; } else { mOutputLength += charsWritten; } while (true) { char *lineEnd = strchr(mOutputString, '\r'); if (lineEnd == nullptr) { break; } *lineEnd = '\0'; if (lineEnd > mOutputString) { otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Output: %s", mOutputString); } lineEnd++; while ((*lineEnd == '\n') || (*lineEnd == '\r')) { lineEnd++; } // Example of the pointers and lengths. // // - mOutputString = "hi\r\nmore" // - mOutputLength = 8 // - lineEnd = &mOutputString[4] // // // 0 1 2 3 4 5 6 7 8 9 // +----+----+----+----+----+----+----+----+----+--- // | h | i | \r | \n | m | o | r | e | \0 | // +----+----+----+----+----+----+----+----+----+--- // ^ ^ // | | // lineEnd mOutputString[mOutputLength] // // // New length is `&mOutputString[8] - &mOutputString[4] -> 4`. // // We move (newLen + 1 = 5) chars from `lineEnd` to start of // `mOutputString` which will include the `\0` char. // // If `lineEnd` and `mOutputString[mOutputLength]` are the same // the code works correctly as well (new length set to zero and // the `\0` is copied). mOutputLength = static_cast(&mOutputString[mOutputLength] - lineEnd); memmove(mOutputString, lineEnd, mOutputLength + 1); } if (truncated) { otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Output: %s ...", mOutputString); mOutputLength = 0; } exit: va_end(args); #endif // OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE } #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE void Utils::LogInput(const Arg *aArgs) { String inputString; for (bool isFirst = true; !aArgs->IsEmpty(); aArgs++, isFirst = false) { inputString.Append(isFirst ? "%s" : " %s", aArgs->GetCString()); } otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Input: %s", inputString.AsCString()); } #endif void Utils::OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[]) { for (uint8_t index = 0; index < aNumColumns; index++) { const char *title = aTitles[index]; uint8_t width = aWidths[index]; size_t titleLength = strlen(title); if (titleLength + 2 <= width) { // `title` fits in column width so we write it with extra space // at beginning and end ("| Title |"). OutputFormat("| %*s", -static_cast(width - 1), title); } else { // Use narrow style (no space at beginning) and write as many // chars from `title` as it can fit in the given column width // ("|Title|"). OutputFormat("|%*.*s", -static_cast(width), width, title); } } OutputLine("|"); OutputTableSeparator(aNumColumns, aWidths); } void Utils::OutputTableSeparator(uint8_t aNumColumns, const uint8_t aWidths[]) { for (uint8_t index = 0; index < aNumColumns; index++) { OutputFormat("+"); for (uint8_t width = aWidths[index]; width != 0; width--) { OutputFormat("-"); } } OutputLine("+"); } otError Utils::ParseEnableOrDisable(const Arg &aArg, bool &aEnable) { otError error = OT_ERROR_NONE; if (aArg == "enable") { aEnable = true; } else if (aArg == "disable") { aEnable = false; } else { error = OT_ERROR_INVALID_COMMAND; } return error; } otError Utils::ProcessEnableDisable(Arg aArgs[], SetEnabledHandler aSetEnabledHandler) { otError error = OT_ERROR_NONE; bool enable; if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE) { aSetEnabledHandler(GetInstancePtr(), enable); } else { error = OT_ERROR_INVALID_COMMAND; } return error; } otError Utils::ProcessEnableDisable(Arg aArgs[], SetEnabledHandlerFailable aSetEnabledHandler) { otError error = OT_ERROR_NONE; bool enable; if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE) { error = aSetEnabledHandler(GetInstancePtr(), enable); } else { error = OT_ERROR_INVALID_COMMAND; } return error; } otError Utils::ProcessEnableDisable(Arg aArgs[], IsEnabledHandler aIsEnabledHandler, SetEnabledHandler aSetEnabledHandler) { otError error = OT_ERROR_NONE; if (aArgs[0].IsEmpty()) { OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr())); } else { error = ProcessEnableDisable(aArgs, aSetEnabledHandler); } return error; } otError Utils::ProcessEnableDisable(Arg aArgs[], IsEnabledHandler aIsEnabledHandler, SetEnabledHandlerFailable aSetEnabledHandler) { otError error = OT_ERROR_NONE; if (aArgs[0].IsEmpty()) { OutputEnabledDisabledStatus(aIsEnabledHandler(GetInstancePtr())); } else { error = ProcessEnableDisable(aArgs, aSetEnabledHandler); } return error; } otError Utils::ParseJoinerDiscerner(Arg &aArg, otJoinerDiscerner &aDiscerner) { otError error; char *separator; VerifyOrExit(!aArg.IsEmpty(), error = OT_ERROR_INVALID_ARGS); separator = strstr(aArg.GetCString(), "/"); VerifyOrExit(separator != nullptr, error = OT_ERROR_NOT_FOUND); SuccessOrExit(error = ot::Utils::CmdLineParser::ParseAsUint8(separator + 1, aDiscerner.mLength)); VerifyOrExit(aDiscerner.mLength > 0 && aDiscerner.mLength <= 64, error = OT_ERROR_INVALID_ARGS); *separator = '\0'; error = aArg.ParseAsUint64(aDiscerner.mValue); exit: return error; } otError Utils::ParsePreference(const Arg &aArg, otRoutePreference &aPreference) { otError error = OT_ERROR_NONE; if (aArg == "high") { aPreference = OT_ROUTE_PREFERENCE_HIGH; } else if (aArg == "med") { aPreference = OT_ROUTE_PREFERENCE_MED; } else if (aArg == "low") { aPreference = OT_ROUTE_PREFERENCE_LOW; } else { error = OT_ERROR_INVALID_ARGS; } return error; } const char *Utils::PreferenceToString(signed int aPreference) { const char *str = ""; switch (aPreference) { case OT_ROUTE_PREFERENCE_LOW: str = "low"; break; case OT_ROUTE_PREFERENCE_MED: str = "med"; break; case OT_ROUTE_PREFERENCE_HIGH: str = "high"; break; default: break; } return str; } #if OPENTHREAD_FTD || OPENTHREAD_MTD otError Utils::ParseToIp6Address(otInstance *aInstance, const Arg &aArg, otIp6Address &aAddress, bool &aSynthesized) { Error error = OT_ERROR_NONE; VerifyOrExit(!aArg.IsEmpty(), error = OT_ERROR_INVALID_ARGS); error = aArg.ParseAsIp6Address(aAddress); aSynthesized = false; if (error != OT_ERROR_NONE) { // It might be an IPv4 address, let's have a try. otIp4Address ip4Address; // Do not touch the error value if we failed to parse it as an IPv4 address. SuccessOrExit(aArg.ParseAsIp4Address(ip4Address)); SuccessOrExit(error = otNat64SynthesizeIp6Address(aInstance, &ip4Address, &aAddress)); aSynthesized = true; } exit: return error; } #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE otError Utils::ParsePrefix(Arg aArgs[], otBorderRouterConfig &aConfig) { otError error = OT_ERROR_NONE; ClearAllBytes(aConfig); SuccessOrExit(error = aArgs[0].ParseAsIp6Prefix(aConfig.mPrefix)); aArgs++; for (; !aArgs->IsEmpty(); aArgs++) { otRoutePreference preference; if (ParsePreference(*aArgs, preference) == OT_ERROR_NONE) { aConfig.mPreference = preference; } else { for (char *arg = aArgs->GetCString(); *arg != '\0'; arg++) { switch (*arg) { case 'p': aConfig.mPreferred = true; break; case 'a': aConfig.mSlaac = true; break; case 'd': aConfig.mDhcp = true; break; case 'c': aConfig.mConfigure = true; break; case 'r': aConfig.mDefaultRoute = true; break; case 'o': aConfig.mOnMesh = true; break; case 's': aConfig.mStable = true; break; case 'n': aConfig.mNdDns = true; break; #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE case 'D': aConfig.mDp = true; break; #endif case '-': break; default: ExitNow(error = OT_ERROR_INVALID_ARGS); } } } } exit: return error; } otError Utils::ParseRoute(Arg aArgs[], otExternalRouteConfig &aConfig) { otError error = OT_ERROR_NONE; ClearAllBytes(aConfig); SuccessOrExit(error = aArgs[0].ParseAsIp6Prefix(aConfig.mPrefix)); aArgs++; for (; !aArgs->IsEmpty(); aArgs++) { otRoutePreference preference; if (ParsePreference(*aArgs, preference) == OT_ERROR_NONE) { aConfig.mPreference = preference; } else { for (char *arg = aArgs->GetCString(); *arg != '\0'; arg++) { switch (*arg) { case 's': aConfig.mStable = true; break; case 'n': aConfig.mNat64 = true; break; case 'a': aConfig.mAdvPio = true; break; case '-': break; default: ExitNow(error = OT_ERROR_INVALID_ARGS); } } } } exit: return error; } #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE #endif // #if OPENTHREAD_FTD || OPENTHREAD_MTD const char *Utils::LinkModeToString(const otLinkModeConfig &aLinkMode, char (&aStringBuffer)[kLinkModeStringSize]) { char *flagsPtr = &aStringBuffer[0]; if (aLinkMode.mRxOnWhenIdle) { *flagsPtr++ = 'r'; } if (aLinkMode.mDeviceType) { *flagsPtr++ = 'd'; } if (aLinkMode.mNetworkData) { *flagsPtr++ = 'n'; } if (flagsPtr == &aStringBuffer[0]) { *flagsPtr++ = '-'; } *flagsPtr = '\0'; return aStringBuffer; } const char *Utils::AddressOriginToString(uint8_t aOrigin) { static const char *const kOriginStrings[4] = { "thread", // 0, OT_ADDRESS_ORIGIN_THREAD "slaac", // 1, OT_ADDRESS_ORIGIN_SLAAC "dhcp6", // 2, OT_ADDRESS_ORIGIN_DHCPV6 "manual", // 3, OT_ADDRESS_ORIGIN_MANUAL }; static_assert(0 == OT_ADDRESS_ORIGIN_THREAD, "OT_ADDRESS_ORIGIN_THREAD value is incorrect"); static_assert(1 == OT_ADDRESS_ORIGIN_SLAAC, "OT_ADDRESS_ORIGIN_SLAAC value is incorrect"); static_assert(2 == OT_ADDRESS_ORIGIN_DHCPV6, "OT_ADDRESS_ORIGIN_DHCPV6 value is incorrect"); static_assert(3 == OT_ADDRESS_ORIGIN_MANUAL, "OT_ADDRESS_ORIGIN_MANUAL value is incorrect"); return Stringify(aOrigin, kOriginStrings); } } // namespace Cli } // namespace ot