blob: c00fc23f1eae688a8d6dd900f03400197e1c29ce [file] [log] [blame]
/*
* 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 );
}
}
}
/*-----------------------------------------------------------*/