/* | |
* AWS IoT Jobs V1.0.0 | |
* Copyright (C) 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy of | |
* this software and associated documentation files (the "Software"), to deal in | |
* the Software without restriction, including without limitation the rights to | |
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |
* the Software, and to permit persons to whom the Software is furnished to do so, | |
* subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | |
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | |
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
/** | |
* @file aws_iot_jobs_serialize.c | |
* @brief Implements functions that generate and parse Jobs JSON documents. | |
*/ | |
/* The config header is always included first. */ | |
#include "iot_config.h" | |
/* Standard includes. */ | |
#include <stdio.h> | |
#include <string.h> | |
/* Jobs internal include. */ | |
#include "private/aws_iot_jobs_internal.h" | |
/* Error handling include. */ | |
#include "iot_error.h" | |
/* JSON utilities include. */ | |
#include "aws_iot_doc_parser.h" | |
/** | |
* @brief Minimum length of a Jobs request. | |
* | |
* At the very least, the request will contain: {"clientToken":""} | |
*/ | |
#define MINIMUM_REQUEST_LENGTH ( AWS_IOT_CLIENT_TOKEN_KEY_LENGTH + 7 ) | |
/** | |
* @brief The length of client tokens generated by this library. | |
*/ | |
#define CLIENT_TOKEN_AUTOGENERATE_LENGTH ( 8 ) | |
/** | |
* @brief JSON key representing Jobs status. | |
*/ | |
#define STATUS_KEY "status" | |
/** | |
* @brief Length of #STATUS_KEY. | |
*/ | |
#define STATUS_KEY_LENGTH ( sizeof( STATUS_KEY ) - 1 ) | |
/** | |
* @brief JSON key representing Jobs status details. | |
*/ | |
#define STATUS_DETAILS_KEY "statusDetails" | |
/** | |
* @brief Length of #STATUS_DETAILS_KEY. | |
*/ | |
#define STATUS_DETAILS_KEY_LENGTH ( sizeof( STATUS_DETAILS_KEY ) - 1 ) | |
/** | |
* @brief JSON key representing Jobs expected version. | |
*/ | |
#define EXPECTED_VERSION_KEY "expectedVersion" | |
/** | |
* @brief Length of #EXPECTED_VERSION_KEY. | |
*/ | |
#define EXPECTED_VERSION_KEY_LENGTH ( sizeof( EXPECTED_VERSION_KEY ) - 1 ) | |
/** | |
* @brief Maximum length of the expected version when represented as a string. | |
* | |
* The expected version is a 32-bit unsigned integer. This can be represented in | |
* 10 digits plus a NULL-terminator. | |
*/ | |
#define EXPECTED_VERSION_STRING_LENGTH ( 11 ) | |
/** | |
* @brief JSON key representing Jobs step timeout. | |
*/ | |
#define STEP_TIMEOUT_KEY "stepTimeoutInMinutes" | |
/** | |
* @brief Length of #STEP_TIMEOUT_KEY. | |
*/ | |
#define STEP_TIMEOUT_KEY_LENGTH ( sizeof( STEP_TIMEOUT_KEY ) - 1 ) | |
/** | |
* @brief Maximum length of the step timeout when represented as a string. | |
* | |
* The step timeout is in the range of [-1,10080]. This can be represented as | |
* 5 digits plus a NULL-terminator. | |
*/ | |
#define STEP_TIMEOUT_STRING_LENGTH ( 6 ) | |
/** | |
* @brief JSON key representing the "include Job document" flag. | |
*/ | |
#define INCLUDE_JOB_DOCUMENT_KEY "includeJobDocument" | |
/** | |
* @brief JSON key representing the "include Job Execution state" flag. | |
*/ | |
#define INCLUDE_JOB_EXECUTION_STATE_KEY "includeJobExecutionState" | |
/** | |
* @brief Length of #INCLUDE_JOB_EXECUTION_STATE_KEY. | |
*/ | |
#define INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH ( sizeof( INCLUDE_JOB_EXECUTION_STATE_KEY ) - 1 ) | |
/** | |
* @brief Length of #INCLUDE_JOB_DOCUMENT_KEY. | |
*/ | |
#define INCLUDE_JOB_DOCUMENT_KEY_LENGTH ( sizeof( INCLUDE_JOB_DOCUMENT_KEY ) - 1 ) | |
/** | |
* @brief JSON key representing the Jobs execution number. | |
*/ | |
#define EXECUTION_NUMBER_KEY "executionNumber" | |
/** | |
* @brief Length of #EXECUTION_NUMBER_KEY. | |
*/ | |
#define EXECUTION_NUMBER_KEY_LENGTH ( sizeof( EXECUTION_NUMBER_KEY ) - 1 ) | |
/** | |
* @brief Maximum length of the execution number when represented as a string. | |
* | |
* The execution number is a 32-bit integer. This can be represented in 10 digits, | |
* plus 1 for a possible negative sign, plus a NULL-terminator. | |
*/ | |
#define EXECUTION_NUMBER_STRING_LENGTH ( 12 ) | |
/** | |
* @brief JSON key representing Jobs error code in error responses. | |
*/ | |
#define CODE_KEY "code" | |
/** | |
* @brief Length of #CODE_KEY. | |
*/ | |
#define CODE_KEY_LENGTH ( sizeof( CODE_KEY ) - 1 ) | |
/** | |
* @brief Append a string to a buffer. | |
* | |
* Also updates `copyOffset` with `stringLength`. | |
* | |
* @param[in] pBuffer Start of a buffer. | |
* @param[in] copyOffset Offset in `pBuffer` where `pString` will be placed. | |
* @param[in] pString The string to append. | |
* @param[in] stringLength Length of `pString`. | |
*/ | |
#define APPEND_STRING( pBuffer, copyOffset, pString, stringLength ) \ | |
( void ) memcpy( pBuffer + copyOffset, pString, stringLength ); \ | |
copyOffset += ( size_t ) stringLength; | |
/*-----------------------------------------------------------*/ | |
/** | |
* @brief Place a JSON boolean flag in the given buffer. | |
* | |
* @param[in] pBuffer The buffer where the flag is placed. | |
* @param[in] copyOffset Offset in `pBuffer` where the flag is placed. | |
* @param[in] pFlagName Either #INCLUDE_JOB_DOCUMENT_KEY or #INCLUDE_JOB_EXECUTION_STATE_KEY. | |
* @param[in] flagNameLength Either #INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH or | |
* #INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH | |
* @param[in] value Either `true` or `false`. | |
* | |
* @warning This function does not check the length of `pBuffer`! Any provided | |
* buffer must be large enough to accommodate the flag and value. | |
* | |
* @return A value of `copyOffset` after the flag. | |
*/ | |
static size_t _appendFlag( char * pBuffer, | |
size_t copyOffset, | |
const char * pFlagName, | |
size_t flagNameLength, | |
bool value ); | |
/** | |
* @brief Place Job status details in the given buffer. | |
* | |
* @param[in] pBuffer The buffer where the status details are placed. | |
* @param[in] copyOffset Offset in `pBuffer` where the status details are placed. | |
* @param[in] pStatusDetails The status details to place in the buffer. | |
* @param[in] statusDetailsLength Length of `pStatusDetails`. | |
* | |
* @warning This function does not check the length of `pBuffer`! Any provided | |
* buffer must be large enough to accommodate the status details. | |
* | |
* @return A value of `copyOffset` after the status details. | |
*/ | |
static size_t _appendStatusDetails( char * pBuffer, | |
size_t copyOffset, | |
const char * pStatusDetails, | |
size_t statusDetailsLength ); | |
/** | |
* @brief Place Job execution number in the given buffer. | |
* | |
* @param[in] pBuffer The buffer where the execution number is placed. | |
* @param[in] copyOffset Offset in `pBuffer` where the execution number is placed. | |
* @param[in] pExecutionNumber The execution number to place in the buffer. | |
* @param[in] executionNumberLength Length of `pExecutionNumber`. | |
* | |
* @warning This function does not check the length of `pBuffer`! Any provided | |
* buffer must be large enough to accommodate the execution number. | |
* | |
* @return A value of `copyOffset` after the execution number. | |
*/ | |
static size_t _appendExecutionNumber( char * pBuffer, | |
size_t copyOffset, | |
const char * pExecutionNumber, | |
size_t executionNumberLength ); | |
/** | |
* @brief Place Job step timeout in the given buffer. | |
* | |
* @param[in] pBuffer The buffer where the step timeout is placed. | |
* @param[in] copyOffset Offset in `pBuffer` where the step timeout is placed. | |
* @param[in] pStepTimeout The step timeout to place in the buffer. | |
* @param[in] stepTimeoutLength Length of `pStepTimeout`. | |
* | |
* @warning This function does not check the length of `pBuffer`! Any provided | |
* buffer must be large enough to accommodate the step timeout. | |
* | |
* @return A value of `copyOffset` after the step timeout. | |
*/ | |
static size_t _appendStepTimeout( char * pBuffer, | |
size_t copyOffset, | |
const char * pStepTimeout, | |
size_t stepTimeoutLength ); | |
/** | |
* @brief Place a client token in the given buffer. | |
* | |
* @param[in] pBuffer The buffer where the client token is placed. | |
* @param[in] copyOffset Offset in `pBuffer` where client token is placed. | |
* @param[in] pRequestInfo Contains information on a client token to place. | |
* @param[out] pOperation Location and length of client token are written here. | |
* | |
* @warning This function does not check the length of `pBuffer`! Any provided | |
* buffer must be large enough to accommodate #CLIENT_TOKEN_AUTOGENERATE_LENGTH | |
* characters. | |
* | |
* @return A value of `copyOffset` after the client token. | |
*/ | |
static size_t _appendClientToken( char * pBuffer, | |
size_t copyOffset, | |
const AwsIotJobsRequestInfo_t * pRequestInfo, | |
_jobsOperation_t * pOperation ); | |
/** | |
* @brief Generates a request JSON for a GET PENDING operation. | |
* | |
* @param[in] pRequestInfo Common Jobs request parameters. | |
* @param[in] pOperation Operation associated with the Jobs request. | |
* | |
* @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY | |
*/ | |
static AwsIotJobsError_t _generateGetPendingRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
_jobsOperation_t * pOperation ); | |
/** | |
* @brief Generates a request JSON for a START NEXT operation. | |
* | |
* @param[in] pRequestInfo Common Jobs request parameters. | |
* @param[in] pUpdateInfo Jobs update parameters. | |
* @param[in] pOperation Operation associated with the Jobs request. | |
* | |
* @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY | |
*/ | |
static AwsIotJobsError_t _generateStartNextRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
const AwsIotJobsUpdateInfo_t * pUpdateInfo, | |
_jobsOperation_t * pOperation ); | |
/** | |
* @brief Generates a request JSON for a DESCRIBE operation. | |
* | |
* @param[in] pRequestInfo Common jobs request parameters. | |
* @param[in] executionNumber Job execution number to include in request. | |
* @param[in] includeJobDocument Whether the response should include the Job document. | |
* @param[in] pOperation Operation associated with the Jobs request. | |
* | |
* @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY. | |
*/ | |
static AwsIotJobsError_t _generateDescribeRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
int32_t executionNumber, | |
bool includeJobDocument, | |
_jobsOperation_t * pOperation ); | |
/** | |
* @brief Generates a request JSON for an UPDATE operation. | |
* | |
* @param[in] pRequestInfo Common Jobs request parameters. | |
* @param[in] pUpdateInfo Jobs update parameters. | |
* @param[in] pOperation Operation associated with the Jobs request. | |
*/ | |
static AwsIotJobsError_t _generateUpdateRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
const AwsIotJobsUpdateInfo_t * pUpdateInfo, | |
_jobsOperation_t * pOperation ); | |
/** | |
* @brief Parse an error from a Jobs error document. | |
* | |
* @param[in] pErrorDocument Jobs error document. | |
* @param[in] errorDocumentLength Length of `pErrorDocument`. | |
* | |
* @return A Jobs error code between #AWS_IOT_JOBS_INVALID_TOPIC and | |
* #AWS_IOT_JOBS_TERMINAL_STATE. | |
*/ | |
static AwsIotJobsError_t _parseErrorDocument( const char * pErrorDocument, | |
size_t errorDocumentLength ); | |
/*-----------------------------------------------------------*/ | |
static size_t _appendFlag( char * pBuffer, | |
size_t copyOffset, | |
const char * pFlagName, | |
size_t flagNameLength, | |
bool value ) | |
{ | |
if( value == true ) | |
{ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
pFlagName, | |
flagNameLength ); | |
APPEND_STRING( pBuffer, copyOffset, "\":true,\"", 8 ); | |
} | |
else | |
{ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
pFlagName, | |
flagNameLength ); | |
APPEND_STRING( pBuffer, copyOffset, "\":false,\"", 9 ); | |
} | |
return copyOffset; | |
} | |
/*-----------------------------------------------------------*/ | |
static size_t _appendStatusDetails( char * pBuffer, | |
size_t copyOffset, | |
const char * pStatusDetails, | |
size_t statusDetailsLength ) | |
{ | |
APPEND_STRING( pBuffer, copyOffset, STATUS_DETAILS_KEY, STATUS_DETAILS_KEY_LENGTH ); | |
APPEND_STRING( pBuffer, copyOffset, "\":", 2 ); | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
pStatusDetails, | |
statusDetailsLength ); | |
APPEND_STRING( pBuffer, copyOffset, ",\"", 2 ); | |
return copyOffset; | |
} | |
/*-----------------------------------------------------------*/ | |
static size_t _appendExecutionNumber( char * pBuffer, | |
size_t copyOffset, | |
const char * pExecutionNumber, | |
size_t executionNumberLength ) | |
{ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
EXECUTION_NUMBER_KEY, | |
EXECUTION_NUMBER_KEY_LENGTH ); | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
"\":", | |
2 ); | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
pExecutionNumber, | |
executionNumberLength ); | |
APPEND_STRING( pBuffer, copyOffset, ",\"", 2 ); | |
return copyOffset; | |
} | |
/*-----------------------------------------------------------*/ | |
static size_t _appendStepTimeout( char * pBuffer, | |
size_t copyOffset, | |
const char * pStepTimeout, | |
size_t stepTimeoutLength ) | |
{ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
STEP_TIMEOUT_KEY, | |
STEP_TIMEOUT_KEY_LENGTH ); | |
APPEND_STRING( pBuffer, copyOffset, "\":", 2 ); | |
APPEND_STRING( pBuffer, copyOffset, pStepTimeout, stepTimeoutLength ); | |
APPEND_STRING( pBuffer, copyOffset, ",\"", 2 ); | |
return copyOffset; | |
} | |
/*-----------------------------------------------------------*/ | |
static size_t _appendClientToken( char * pBuffer, | |
size_t copyOffset, | |
const AwsIotJobsRequestInfo_t * pRequestInfo, | |
_jobsOperation_t * pOperation ) | |
{ | |
int clientTokenLength = 0; | |
uint32_t clientToken = 0; | |
/* Place the client token key in the buffer. */ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
AWS_IOT_CLIENT_TOKEN_KEY, | |
AWS_IOT_CLIENT_TOKEN_KEY_LENGTH ); | |
APPEND_STRING( pBuffer, copyOffset, "\":\"", 3 ); | |
/* Set the pointer to the client token. */ | |
pOperation->pClientToken = pBuffer + copyOffset - 1; | |
if( pRequestInfo->pClientToken == AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE ) | |
{ | |
/* Take the address of the given buffer, truncated to 8 characters. This | |
* provides a client token that is very likely to be unique while in use. */ | |
clientToken = ( uint32_t ) ( ( uint64_t ) pBuffer % 100000000ULL ); | |
clientTokenLength = snprintf( pBuffer + copyOffset, | |
CLIENT_TOKEN_AUTOGENERATE_LENGTH + 1, | |
"%08u", clientToken ); | |
AwsIotJobs_Assert( clientTokenLength == CLIENT_TOKEN_AUTOGENERATE_LENGTH ); | |
copyOffset += ( size_t ) clientTokenLength; | |
pOperation->clientTokenLength = CLIENT_TOKEN_AUTOGENERATE_LENGTH + 2; | |
} | |
else | |
{ | |
APPEND_STRING( pBuffer, | |
copyOffset, | |
pRequestInfo->pClientToken, | |
pRequestInfo->clientTokenLength ); | |
pOperation->clientTokenLength = pRequestInfo->clientTokenLength + 2; | |
} | |
return copyOffset; | |
} | |
/*-----------------------------------------------------------*/ | |
static AwsIotJobsError_t _generateGetPendingRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
_jobsOperation_t * pOperation ) | |
{ | |
IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS ); | |
char * pJobsRequest = NULL; | |
size_t copyOffset = 0; | |
size_t requestLength = MINIMUM_REQUEST_LENGTH; | |
/* Add the length of the client token. */ | |
if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE ) | |
{ | |
AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 ); | |
requestLength += pRequestInfo->clientTokenLength; | |
} | |
else | |
{ | |
requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH; | |
} | |
/* Allocate memory for the request JSON. */ | |
pJobsRequest = AwsIotJobs_MallocString( requestLength ); | |
if( pJobsRequest == NULL ) | |
{ | |
IotLogError( "No memory for Jobs GET PENDING request." ); | |
IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY ); | |
} | |
/* Clear the request JSON. */ | |
( void ) memset( pJobsRequest, 0x00, requestLength ); | |
/* Construct the request JSON, which consists of just a clientToken key. */ | |
APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 ); | |
copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 ); | |
/* Set the output parameters. */ | |
pOperation->pJobsRequest = pJobsRequest; | |
pOperation->jobsRequestLength = requestLength; | |
/* Ensure offsets are valid. */ | |
AwsIotJobs_Assert( copyOffset == requestLength ); | |
AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest ); | |
AwsIotJobs_Assert( pOperation->pClientToken < | |
pOperation->pJobsRequest + pOperation->jobsRequestLength ); | |
IotLogDebug( "Jobs GET PENDING request: %.*s", | |
pOperation->jobsRequestLength, | |
pOperation->pJobsRequest ); | |
IOT_FUNCTION_EXIT_NO_CLEANUP(); | |
} | |
/*-----------------------------------------------------------*/ | |
static AwsIotJobsError_t _generateStartNextRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
const AwsIotJobsUpdateInfo_t * pUpdateInfo, | |
_jobsOperation_t * pOperation ) | |
{ | |
IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS ); | |
char * pJobsRequest = NULL; | |
size_t copyOffset = 0; | |
size_t requestLength = MINIMUM_REQUEST_LENGTH; | |
char pStepTimeout[ STEP_TIMEOUT_STRING_LENGTH ] = { 0 }; | |
int stepTimeoutLength = 0; | |
/* Add the length of status details if provided. */ | |
if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS ) | |
{ | |
/* Add 4 for the 2 quotes, colon, and comma. */ | |
requestLength += STATUS_DETAILS_KEY_LENGTH + 4; | |
requestLength += pUpdateInfo->statusDetailsLength; | |
} | |
if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT ) | |
{ | |
/* Calculate the length of the step timeout. Add 4 for the 2 quotes, colon, and comma. */ | |
requestLength += STEP_TIMEOUT_KEY_LENGTH + 4; | |
if( pUpdateInfo->stepTimeoutInMinutes == AWS_IOT_JOBS_CANCEL_TIMEOUT ) | |
{ | |
/* Step timeout will be set to -1. */ | |
pStepTimeout[ 0 ] = '-'; | |
pStepTimeout[ 1 ] = '1'; | |
stepTimeoutLength = 2; | |
} | |
else | |
{ | |
/* Convert the step timeout to a string. */ | |
stepTimeoutLength = snprintf( pStepTimeout, | |
STEP_TIMEOUT_STRING_LENGTH, | |
"%d", | |
pUpdateInfo->stepTimeoutInMinutes ); | |
AwsIotJobs_Assert( stepTimeoutLength > 0 ); | |
AwsIotJobs_Assert( stepTimeoutLength < STEP_TIMEOUT_STRING_LENGTH ); | |
} | |
requestLength += ( size_t ) stepTimeoutLength; | |
} | |
/* Add the length of the client token. */ | |
if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE ) | |
{ | |
AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 ); | |
requestLength += pRequestInfo->clientTokenLength; | |
} | |
else | |
{ | |
requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH; | |
} | |
/* Allocate memory for the request JSON. */ | |
pJobsRequest = AwsIotJobs_MallocString( requestLength ); | |
if( pJobsRequest == NULL ) | |
{ | |
IotLogError( "No memory for Jobs START NEXT request." ); | |
IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY ); | |
} | |
/* Clear the request JSON. */ | |
( void ) memset( pJobsRequest, 0x00, requestLength ); | |
/* Construct the request JSON. */ | |
APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 ); | |
/* Add status details if present. */ | |
if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS ) | |
{ | |
copyOffset = _appendStatusDetails( pJobsRequest, | |
copyOffset, | |
pUpdateInfo->pStatusDetails, | |
pUpdateInfo->statusDetailsLength ); | |
} | |
/* Add step timeout if present. */ | |
if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT ) | |
{ | |
copyOffset = _appendStepTimeout( pJobsRequest, | |
copyOffset, | |
pStepTimeout, | |
stepTimeoutLength ); | |
} | |
/* Add client token. */ | |
copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 ); | |
/* Set the output parameters. */ | |
pOperation->pJobsRequest = pJobsRequest; | |
pOperation->jobsRequestLength = requestLength; | |
/* Ensure offsets are valid. */ | |
AwsIotJobs_Assert( copyOffset == requestLength ); | |
AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest ); | |
AwsIotJobs_Assert( pOperation->pClientToken < | |
pOperation->pJobsRequest + pOperation->jobsRequestLength ); | |
IotLogDebug( "Jobs START NEXT request: %.*s", | |
pOperation->jobsRequestLength, | |
pOperation->pJobsRequest ); | |
IOT_FUNCTION_EXIT_NO_CLEANUP(); | |
} | |
/*-----------------------------------------------------------*/ | |
static AwsIotJobsError_t _generateDescribeRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
int32_t executionNumber, | |
bool includeJobDocument, | |
_jobsOperation_t * pOperation ) | |
{ | |
IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS ); | |
char * pJobsRequest = NULL; | |
size_t copyOffset = 0; | |
size_t requestLength = MINIMUM_REQUEST_LENGTH; | |
char pExecutionNumber[ EXECUTION_NUMBER_STRING_LENGTH ] = { 0 }; | |
int executionNumberLength = 0; | |
/* Add the "include job document" flag if false. The default value is true, | |
* so the flag is not needed if true. */ | |
if( includeJobDocument == false ) | |
{ | |
/* Add the length of "includeJobDocument" plus 4 for 2 quotes, a colon, | |
* and a comma. */ | |
requestLength += INCLUDE_JOB_DOCUMENT_KEY_LENGTH + 4; | |
/* Add the length of "false". */ | |
requestLength += 5; | |
} | |
/* Add the length of the execution number if present. */ | |
if( executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER ) | |
{ | |
/* Convert the execution number to a string. */ | |
executionNumberLength = snprintf( pExecutionNumber, | |
EXECUTION_NUMBER_STRING_LENGTH, | |
"%d", | |
executionNumber ); | |
AwsIotJobs_Assert( executionNumberLength > 0 ); | |
AwsIotJobs_Assert( executionNumberLength < EXECUTION_NUMBER_STRING_LENGTH ); | |
requestLength += EXECUTION_NUMBER_KEY_LENGTH + 4; | |
requestLength += ( size_t ) executionNumberLength; | |
} | |
/* Add the length of the client token. */ | |
if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE ) | |
{ | |
AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 ); | |
requestLength += pRequestInfo->clientTokenLength; | |
} | |
else | |
{ | |
requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH; | |
} | |
/* Allocate memory for the request JSON. */ | |
pJobsRequest = AwsIotJobs_MallocString( requestLength ); | |
if( pJobsRequest == NULL ) | |
{ | |
IotLogError( "No memory for Jobs DESCRIBE request." ); | |
IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY ); | |
} | |
/* Clear the request JSON. */ | |
( void ) memset( pJobsRequest, 0x00, requestLength ); | |
/* Construct the request JSON. */ | |
APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 ); | |
/* Add the "include job document" flag if false. */ | |
if( includeJobDocument == false ) | |
{ | |
copyOffset = _appendFlag( pJobsRequest, | |
copyOffset, | |
INCLUDE_JOB_DOCUMENT_KEY, | |
INCLUDE_JOB_DOCUMENT_KEY_LENGTH, | |
false ); | |
} | |
/* Add the length of the execution number if present. */ | |
if( executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER ) | |
{ | |
copyOffset = _appendExecutionNumber( pJobsRequest, | |
copyOffset, | |
pExecutionNumber, | |
( size_t ) executionNumberLength ); | |
} | |
/* Add client token. */ | |
copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 ); | |
/* Set the output parameters. */ | |
pOperation->pJobsRequest = pJobsRequest; | |
pOperation->jobsRequestLength = requestLength; | |
/* Ensure offsets are valid. */ | |
AwsIotJobs_Assert( copyOffset == requestLength ); | |
AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest ); | |
AwsIotJobs_Assert( pOperation->pClientToken < | |
pOperation->pJobsRequest + pOperation->jobsRequestLength ); | |
IotLogDebug( "Jobs DESCRIBE request: %.*s", | |
pOperation->jobsRequestLength, | |
pOperation->pJobsRequest ); | |
IOT_FUNCTION_EXIT_NO_CLEANUP(); | |
} | |
/*-----------------------------------------------------------*/ | |
static AwsIotJobsError_t _generateUpdateRequest( const AwsIotJobsRequestInfo_t * pRequestInfo, | |
const AwsIotJobsUpdateInfo_t * pUpdateInfo, | |
_jobsOperation_t * pOperation ) | |
{ | |
IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS ); | |
char * pJobsRequest = NULL; | |
size_t copyOffset = 0; | |
size_t requestLength = MINIMUM_REQUEST_LENGTH; | |
const char * pStatus = NULL; | |
size_t statusLength = 0; | |
char pExpectedVersion[ EXPECTED_VERSION_STRING_LENGTH ] = { 0 }; | |
char pExecutionNumber[ EXECUTION_NUMBER_STRING_LENGTH ] = { 0 }; | |
char pStepTimeout[ STEP_TIMEOUT_STRING_LENGTH ] = { 0 }; | |
int expectedVersionLength = 0, executionNumberLength = 0, stepTimeoutLength = 0; | |
/* Determine the status string and length to report to the Jobs service. | |
* Add 6 for the 4 quotes, colon, and comma. */ | |
requestLength += STATUS_KEY_LENGTH + 6; | |
switch( pUpdateInfo->newStatus ) | |
{ | |
case AWS_IOT_JOB_STATE_IN_PROGRESS: | |
pStatus = "IN_PROGRESS"; | |
break; | |
case AWS_IOT_JOB_STATE_FAILED: | |
pStatus = "FAILED"; | |
break; | |
case AWS_IOT_JOB_STATE_SUCCEEDED: | |
pStatus = "SUCCEEDED"; | |
break; | |
default: | |
/* The only remaining valid state is REJECTED. */ | |
AwsIotJobs_Assert( pUpdateInfo->newStatus == AWS_IOT_JOB_STATE_REJECTED ); | |
pStatus = "REJECTED"; | |
break; | |
} | |
statusLength = strlen( pStatus ); | |
requestLength += statusLength; | |
/* Add the length of status details if provided. */ | |
if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS ) | |
{ | |
/* Add 4 for the 2 quotes, colon, and comma. */ | |
requestLength += STATUS_DETAILS_KEY_LENGTH + 4; | |
requestLength += pUpdateInfo->statusDetailsLength; | |
} | |
/* Add the expected version if provided. */ | |
if( pUpdateInfo->expectedVersion != AWS_IOT_JOBS_NO_VERSION ) | |
{ | |
/* Convert the expected version to a string. */ | |
expectedVersionLength = snprintf( pExpectedVersion, | |
EXPECTED_VERSION_STRING_LENGTH, | |
"%u", | |
pUpdateInfo->expectedVersion ); | |
AwsIotJobs_Assert( expectedVersionLength > 0 ); | |
AwsIotJobs_Assert( expectedVersionLength < EXPECTED_VERSION_STRING_LENGTH ); | |
/* Add 6 for the 4 quotes, colon, and comma. */ | |
requestLength += EXPECTED_VERSION_KEY_LENGTH + 6; | |
requestLength += ( size_t ) expectedVersionLength; | |
} | |
/* Add the length of the execution number if present. */ | |
if( pUpdateInfo->executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER ) | |
{ | |
/* Convert the execution number to a string. */ | |
executionNumberLength = snprintf( pExecutionNumber, | |
EXECUTION_NUMBER_STRING_LENGTH, | |
"%d", | |
pUpdateInfo->executionNumber ); | |
AwsIotJobs_Assert( executionNumberLength > 0 ); | |
AwsIotJobs_Assert( executionNumberLength < EXECUTION_NUMBER_STRING_LENGTH ); | |
requestLength += EXECUTION_NUMBER_KEY_LENGTH + 4; | |
requestLength += ( size_t ) executionNumberLength; | |
} | |
/* Add the flags if true. The default values are false, so the flags are not | |
* needed if false. */ | |
if( pUpdateInfo->includeJobExecutionState == true ) | |
{ | |
/* Add the length of "includeJobExecutionState" plus 4 for 2 quotes, a colon, | |
* and a comma. */ | |
requestLength += INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH + 4; | |
/* Add the length of "true". */ | |
requestLength += 4; | |
} | |
if( pUpdateInfo->includeJobDocument == true ) | |
{ | |
/* Add the length of "includeJobDocument" plus 4 for 2 quotes, a colon, | |
* and a comma. */ | |
requestLength += INCLUDE_JOB_DOCUMENT_KEY_LENGTH + 4; | |
/* Add the length of "true". */ | |
requestLength += 4; | |
} | |
/* Add the step timeout if provided. */ | |
if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT ) | |
{ | |
/* Calculate the length of the step timeout. Add 4 for the 2 quotes, colon, and comma. */ | |
requestLength += STEP_TIMEOUT_KEY_LENGTH + 4; | |
if( pUpdateInfo->stepTimeoutInMinutes == AWS_IOT_JOBS_CANCEL_TIMEOUT ) | |
{ | |
/* Step timeout will be set to -1. */ | |
pStepTimeout[ 0 ] = '-'; | |
pStepTimeout[ 1 ] = '1'; | |
stepTimeoutLength = 2; | |
} | |
else | |
{ | |
/* Convert the step timeout to a string. */ | |
stepTimeoutLength = snprintf( pStepTimeout, | |
STEP_TIMEOUT_STRING_LENGTH, | |
"%d", | |
pUpdateInfo->stepTimeoutInMinutes ); | |
AwsIotJobs_Assert( stepTimeoutLength > 0 ); | |
AwsIotJobs_Assert( stepTimeoutLength < STEP_TIMEOUT_STRING_LENGTH ); | |
} | |
requestLength += ( size_t ) stepTimeoutLength; | |
} | |
/* Add the length of the client token. */ | |
if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE ) | |
{ | |
AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 ); | |
requestLength += pRequestInfo->clientTokenLength; | |
} | |
else | |
{ | |
requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH; | |
} | |
/* Allocate memory for the request JSON. */ | |
pJobsRequest = AwsIotJobs_MallocString( requestLength ); | |
if( pJobsRequest == NULL ) | |
{ | |
IotLogError( "No memory for Jobs UPDATE request." ); | |
IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY ); | |
} | |
/* Clear the request JSON. */ | |
( void ) memset( pJobsRequest, 0x00, requestLength ); | |
/* Construct the request JSON. */ | |
APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 ); | |
/* Add the status. */ | |
APPEND_STRING( pJobsRequest, copyOffset, STATUS_KEY, STATUS_KEY_LENGTH ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\":\"", 3 ); | |
APPEND_STRING( pJobsRequest, copyOffset, pStatus, statusLength ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\",\"", 3 ); | |
/* Add status details if present. */ | |
if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS ) | |
{ | |
copyOffset = _appendStatusDetails( pJobsRequest, | |
copyOffset, | |
pUpdateInfo->pStatusDetails, | |
pUpdateInfo->statusDetailsLength ); | |
} | |
/* Add expected version. */ | |
if( pUpdateInfo->expectedVersion != AWS_IOT_JOBS_NO_VERSION ) | |
{ | |
APPEND_STRING( pJobsRequest, | |
copyOffset, | |
EXPECTED_VERSION_KEY, | |
EXPECTED_VERSION_KEY_LENGTH ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\":\"", 3 ); | |
APPEND_STRING( pJobsRequest, copyOffset, pExpectedVersion, expectedVersionLength ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\",\"", 3 ); | |
} | |
/* Add execution number. */ | |
if( pUpdateInfo->executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER ) | |
{ | |
copyOffset = _appendExecutionNumber( pJobsRequest, | |
copyOffset, | |
pExecutionNumber, | |
executionNumberLength ); | |
} | |
/* Add flags if not default values. */ | |
if( pUpdateInfo->includeJobExecutionState == true ) | |
{ | |
copyOffset = _appendFlag( pJobsRequest, | |
copyOffset, | |
INCLUDE_JOB_EXECUTION_STATE_KEY, | |
INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH, | |
true ); | |
} | |
if( pUpdateInfo->includeJobDocument == true ) | |
{ | |
copyOffset = _appendFlag( pJobsRequest, | |
copyOffset, | |
INCLUDE_JOB_DOCUMENT_KEY, | |
INCLUDE_JOB_DOCUMENT_KEY_LENGTH, | |
true ); | |
} | |
/* Add step timeout if provided. */ | |
if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT ) | |
{ | |
copyOffset = _appendStepTimeout( pJobsRequest, | |
copyOffset, | |
pStepTimeout, | |
stepTimeoutLength ); | |
} | |
/* Add the client token. */ | |
copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation ); | |
APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 ); | |
/* Set the output parameters. */ | |
pOperation->pJobsRequest = pJobsRequest; | |
pOperation->jobsRequestLength = requestLength; | |
/* Ensure offsets are valid. */ | |
AwsIotJobs_Assert( copyOffset == requestLength ); | |
AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest ); | |
AwsIotJobs_Assert( pOperation->pClientToken < | |
pOperation->pJobsRequest + pOperation->jobsRequestLength ); | |
IotLogDebug( "Jobs UPDATE request: %.*s", | |
pOperation->jobsRequestLength, | |
pOperation->pJobsRequest ); | |
IOT_FUNCTION_EXIT_NO_CLEANUP(); | |
} | |
/*-----------------------------------------------------------*/ | |
static AwsIotJobsError_t _parseErrorDocument( const char * pErrorDocument, | |
size_t errorDocumentLength ) | |
{ | |
IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING ); | |
const char * pCode = NULL; | |
size_t codeLength = 0; | |
/* Find the error code. */ | |
if( AwsIotDocParser_FindValue( pErrorDocument, | |
errorDocumentLength, | |
CODE_KEY, | |
CODE_KEY_LENGTH, | |
&pCode, | |
&codeLength ) == false ) | |
{ | |
IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_RESPONSE ); | |
} | |
/* Match the JSON error code to a Jobs return value. Assume invalid status | |
* unless matched.*/ | |
status = AWS_IOT_JOBS_BAD_RESPONSE; | |
switch( codeLength ) | |
{ | |
/* InvalidJson */ | |
case 13: | |
if( strncmp( "\"InvalidJson\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_INVALID_JSON; | |
} | |
break; | |
/* InvalidTopic */ | |
case 14: | |
if( strncmp( "\"InvalidTopic\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_INVALID_TOPIC; | |
} | |
break; | |
/* InternalError */ | |
case 15: | |
if( strncmp( "\"InternalError\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_INTERNAL_ERROR; | |
} | |
break; | |
/* InvalidRequest */ | |
case 16: | |
if( strncmp( "\"InvalidRequest\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_INVALID_REQUEST; | |
} | |
break; | |
/* VersionMismatch */ | |
case 17: | |
if( strncmp( "\"VersionMismatch\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_VERSION_MISMATCH; | |
} | |
break; | |
/* ResourceNotFound, RequestThrottled */ | |
case 18: | |
if( strncmp( "\"ResourceNotFound\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_NOT_FOUND; | |
} | |
else if( strncmp( "\"RequestThrottled\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_THROTTLED; | |
} | |
break; | |
/* TerminalStateReached */ | |
case 22: | |
if( strncmp( "\"TerminalStateReached\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_TERMINAL_STATE; | |
} | |
break; | |
/* InvalidStateTransition */ | |
case 24: | |
if( strncmp( "\"InvalidStateTransition\"", pCode, codeLength ) == 0 ) | |
{ | |
status = AWS_IOT_JOBS_INVALID_STATE; | |
} | |
break; | |
default: | |
break; | |
} | |
IOT_FUNCTION_EXIT_NO_CLEANUP(); | |
} | |
/*-----------------------------------------------------------*/ | |
AwsIotJobsError_t _AwsIotJobs_GenerateJsonRequest( _jobsOperationType_t type, | |
const AwsIotJobsRequestInfo_t * pRequestInfo, | |
const _jsonRequestContents_t * pRequestContents, | |
_jobsOperation_t * pOperation ) | |
{ | |
AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING; | |
/* Generate request based on the Job operation type. */ | |
switch( type ) | |
{ | |
case JOBS_GET_PENDING: | |
status = _generateGetPendingRequest( pRequestInfo, pOperation ); | |
break; | |
case JOBS_START_NEXT: | |
status = _generateStartNextRequest( pRequestInfo, | |
pRequestContents->pUpdateInfo, | |
pOperation ); | |
break; | |
case JOBS_DESCRIBE: | |
status = _generateDescribeRequest( pRequestInfo, | |
pRequestContents->describe.executionNumber, | |
pRequestContents->describe.includeJobDocument, | |
pOperation ); | |
break; | |
default: | |
/* The only remaining valid type is UPDATE. */ | |
AwsIotJobs_Assert( type == JOBS_UPDATE ); | |
status = _generateUpdateRequest( pRequestInfo, | |
pRequestContents->pUpdateInfo, | |
pOperation ); | |
break; | |
} | |
return status; | |
} | |
/*-----------------------------------------------------------*/ | |
void _AwsIotJobs_ParseResponse( AwsIotStatus_t status, | |
const char * pResponse, | |
size_t responseLength, | |
_jobsOperation_t * pOperation ) | |
{ | |
AwsIotJobs_Assert( pOperation->status == AWS_IOT_JOBS_STATUS_PENDING ); | |
/* A non-waitable operation can re-use the pointers from the publish info, | |
* since those are guaranteed to be in-scope throughout the user callback. | |
* But a waitable operation must copy the data from the publish info because | |
* AwsIotJobs_Wait may be called after the MQTT library frees the publish | |
* info. */ | |
if( ( pOperation->flags & AWS_IOT_JOBS_FLAG_WAITABLE ) == 0 ) | |
{ | |
pOperation->pJobsResponse = pResponse; | |
pOperation->jobsResponseLength = responseLength; | |
} | |
else | |
{ | |
IotLogDebug( "Allocating new buffer for waitable Jobs %s.", | |
_pAwsIotJobsOperationNames[ pOperation->type ] ); | |
/* Parameter validation should not have allowed a NULL malloc function. */ | |
AwsIotJobs_Assert( pOperation->mallocResponse != NULL ); | |
/* Allocate a buffer for the retrieved document. */ | |
pOperation->pJobsResponse = pOperation->mallocResponse( responseLength ); | |
if( pOperation->pJobsResponse == NULL ) | |
{ | |
IotLogError( "Failed to allocate buffer for retrieved Jobs %s response.", | |
_pAwsIotJobsOperationNames[ pOperation->type ] ); | |
pOperation->status = AWS_IOT_JOBS_NO_MEMORY; | |
} | |
else | |
{ | |
/* Copy the response. */ | |
( void ) memcpy( ( void * ) pOperation->pJobsResponse, pResponse, responseLength ); | |
pOperation->jobsResponseLength = responseLength; | |
} | |
} | |
/* Set the status of the Jobs operation. */ | |
if( pOperation->status == AWS_IOT_JOBS_STATUS_PENDING ) | |
{ | |
if( status == AWS_IOT_ACCEPTED ) | |
{ | |
pOperation->status = AWS_IOT_JOBS_SUCCESS; | |
} | |
else | |
{ | |
pOperation->status = _parseErrorDocument( pResponse, responseLength ); | |
} | |
} | |
} | |
/*-----------------------------------------------------------*/ |