blob: c124ca5af3dd9c49ae1edf3879bac13c3e6760cf [file]
/*
* Copyright (C) 2026 Michael Brown <mbrown@fensystems.co.uk>.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
* You can also choose to distribute this program under the terms of
* the Unmodified Binary Distribution Licence (as given in the file
* COPYING.UBDL), provided that you have satisfied its requirements.
*/
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#include <stdint.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <ipxe/image.h>
#include <ipxe/base64.h>
#include <ipxe/mime.h>
/** @file
*
* MIME image format
*
* The MIME format is defined in RFC 2045 and RFC 2046 (with reference
* to RFC 822). We treat it firstly as a simple single-member archive
* format where the content is extracted using the specified encoding.
*
* We additionally support multipart MIME as an archive format from
* which we attempt to extract the first body part that has the MIME
* type "text/x-ipxe". This makes it possible to store both
* cloud-init configuration and an iPXE boot script in the same user
* metadata blob for a cloud instance.
*
* Due to its historical origins, MIME is an irritatingly flexible
* format. We do not attempt to support all possible MIME files: only
* those that we might reasonably expect to encounter as cloud
* instance metadata.
*/
static const struct mime_type * mime_type ( const char *name );
/**
* Parse MIME attribute
*
* @v image MIME image
* @v value Header value
* @v eol End of header line
* @v name Attribute name (including terminating equals sign)
* @v attr MIME attribute to update
*/
static void mime_parse_attribute ( struct image *image, const char *value,
const char *eol, const char *name,
struct mime_attribute *attr ) {
const char *match;
/* Locate attribute */
match = strcasestr ( value, name );
if ( ( ! match ) || ( match >= eol ) )
return;
attr->value = ( match + strlen ( name ) );
DBGC2 ( image, "MIME %s found %s\"", image->name, name );
/* Determine attribute length */
if ( attr->value[0] == '"' )
attr->value++;
attr->len = 0;
while ( attr->value[attr->len] && ( attr->value[attr->len] != '"' ) &&
( ! isspace ( attr->value[attr->len] ) ) ) {
DBGC2 ( image, "%c", attr->value[attr->len] );
attr->len++;
}
DBGC2 ( image, "\"\n" );
assert ( &attr->value[attr->len] <= eol );
}
/**
* Parse Content-Type header
*
* @v image MIME image
* @v value Header value
* @v eol End of header line
* @v headers MIME headers to update
*/
static void mime_parse_type ( struct image *image, const char *value,
const char *eol, struct mime_headers *headers ) {
/* Record content type */
headers->type = value;
DBGC2 ( image, "MIME %s found content type\n", image->name );
/* Check for boundary separator */
mime_parse_attribute ( image, value, eol, "boundary=",
&headers->boundary );
}
/**
* Parse Content-Transfer-Encoding header
*
* @v image MIME image
* @v value Header value
* @v eol End of header line
* @v headers MIME headers to update
*/
static void mime_parse_encoding ( struct image *image, const char *value,
const char *eol __unused,
struct mime_headers *headers ) {
/* Record content transfer encoding */
headers->encoding = value;
DBGC2 ( image, "MIME %s found content transfer encoding\n",
image->name );
}
/** Recognised MIME headers */
const struct mime_header mime_headers[] = {
{
.name = "Content-Type:",
.parse = mime_parse_type,
},
{
.name = "Content-Transfer-Encoding:",
.parse = mime_parse_encoding,
},
};
/**
* Identify MIME header
*
* @v line Header line
* @ret header MIME header, or NULL if not recognised
*/
static const struct mime_header * mime_header ( const char *line ) {
const struct mime_header *header;
unsigned int i;
/* Identify MIME header */
for ( i = 0 ; i < ( sizeof ( mime_headers ) /
sizeof ( mime_headers[0] ) ) ; i++ ) {
header = &mime_headers[i];
if ( strncasecmp ( line, header->name,
strlen ( header->name ) ) == 0 ) {
return header;
}
}
return NULL;
}
/**
* Parse MIME headers
*
* @v image MIME image
* @v text Start of MIME headers within image
* @v headers MIME headers to fill in
* @ret rc Return status code
*/
static int mime_parse ( struct image *image, const char *text,
struct mime_headers *headers ) {
const struct mime_header *header;
const char *value;
const char *line;
const char *next;
const char *eol;
/* Initialise headers */
memset ( headers, 0, sizeof ( *headers ) );
/* Parse headers until reaching empty-line separator */
for ( line = text ; ; line = next ) {
/* Locate end of line */
eol = strchr ( line, '\n' );
if ( ! eol ) {
DBGC ( image, "MIME %s premature end of file\n",
image->name );
return -EINVAL;
}
next = ( eol + 1 );
/* Check for empty line */
if ( eol == line )
break;
if ( eol[-1] == '\r' )
eol--;
if ( eol == line )
break;
/* Identify header (if recognised) */
header = mime_header ( line );
if ( ! header )
continue;
/* Locate start of value */
value = ( line + strlen ( header->name ) );
while ( ( *value != '\n' ) && isspace ( *value ) )
value++;
/* Parse header */
header->parse ( image, value, eol, headers );
}
/* Check for mandatory headers */
if ( ! headers->type ) {
DBGC ( image, "MIME %s missing content type\n", image->name );
return -EINVAL;
}
/* Record length of headers */
headers->len = ( next - text );
return 0;
}
/**
* Decode MIME entity with identity encoding
*
* @v image MIME image
* @v headers MIME headers
* @v decoded Decoded image
* @ret rc Return status code
*/
static int mime_decode_identity ( struct image *image,
const struct mime_headers *headers,
struct image *decoded ) {
size_t len;
int rc;
/* Allocate space */
len = ( image->len - headers->len );
if ( ( rc = image_set_len ( decoded, len ) ) != 0 )
return rc;
/* Decode data */
memcpy ( decoded->rwdata, ( image->data + headers->len ), len );
return 0;
}
/**
* Decode MIME entity with Base64 encoding
*
* @v image MIME image
* @v headers MIME headers
* @v decoded Decoded image
* @ret rc Return status code
*/
static int mime_decode_base64 ( struct image *image,
const struct mime_headers *headers,
struct image *decoded ) {
const char *encoded;
size_t max_len;
int len;
int rc;
/* Allocate space for decoded data */
encoded = ( image->text + headers->len );
max_len = base64_decoded_max_len ( encoded );
if ( ( rc = image_set_len ( decoded, max_len ) ) != 0 )
return rc;
/* Decode data */
len = base64_decode ( encoded, decoded->rwdata, decoded->len );
if ( len < 0 ) {
rc = len;
DBGC ( image, "MIME %s could not decode base64: %s\n",
image->name, strerror ( rc ) );
return rc;
}
assert ( ( ( size_t ) len ) <= max_len );
/* Set decoded length */
if ( ( rc = image_set_len ( decoded, len ) ) != 0 )
return rc;
return 0;
}
/** MIME encodings */
static const struct mime_encoding mime_encodings[] = {
{
/* Default encoding */
.name = "7bit",
.decode = mime_decode_identity,
},
{
.name = "8bit",
.decode = mime_decode_identity,
},
{
.name = "binary",
.decode = mime_decode_identity,
},
{
.name = "base64",
.decode = mime_decode_base64,
},
};
/**
* Identify MIME encoding
*
* @v name Encoding name, or NULL to use default
* @ret encoding MIME encoding, or NULL if not recognised
*/
static const struct mime_encoding * mime_encoding ( const char *name ) {
const struct mime_encoding *encoding;
size_t len;
unsigned int i;
/* Use default encoding if no name specified */
if ( ! name )
return &mime_encodings[0];
/* Identify MIME encoding */
for ( i = 0 ; i < ( sizeof ( mime_encodings ) /
sizeof ( mime_encodings[0] ) ) ; i++ ) {
encoding = &mime_encodings[i];
len = strlen ( encoding->name );
if ( ( strncasecmp ( name, encoding->name, len ) == 0 ) &&
( ( name[len] == ';' ) || isspace ( name[len] ) ) ) {
return encoding;
}
}
return NULL;
}
/**
* Extract single MIME entity
*
* @v image MIME image
* @v headers MIME headers
* @v extracted Extracted image
* @ret rc Return status code
*/
static int mime_extract_entity ( struct image *image,
const struct mime_headers *headers,
struct image *extracted ) {
const struct mime_encoding *encoding;
int rc;
/* Identify encoding */
encoding = mime_encoding ( headers->encoding );
if ( ! encoding ) {
DBGC ( image, "MIME %s has unrecognised encoding\n",
image->name );
return -ENOTSUP;
}
DBGC ( image, "MIME %s has encoding %s\n",
image->name, encoding->name );
/* Decode via applicable encoding */
if ( ( rc = encoding->decode ( image, headers, extracted ) ) != 0 )
return rc;
return 0;
}
/**
* Find end of current part in multipart MIME image
*
* @v image MIME image
* @v boundary Boundary separator
* @v pos Current position within image
* @ret end End of current part, or NULL if not found
*/
static const char * mime_part_end ( struct image *image,
const struct mime_attribute *boundary,
const char *pos ) {
/* Locate boundary marker */
while ( 1 ) {
pos = strstr ( pos, "\n--" );
if ( ! pos )
break;
pos += ( 1 /* newline */ );
if ( strncmp ( ( pos + 2 /* -- */ ), boundary->value,
boundary->len ) != 0 ) {
continue;
}
return pos;
}
DBGC ( image, "MIME %s missing boundary marker\n", image->name );
return NULL;
}
/**
* Find start of next part in multipart MIME image
*
* @v image MIME image
* @v boundary Boundary separator
* @v pos End of current part within image
* @v next Start of next part, or NULL if not found
*/
static const char * mime_part_next ( struct image *image,
const struct mime_attribute *boundary,
const char *pos ) {
/* Skip boundary marker */
pos += ( 2 /* -- */ + boundary->len );
/* Check for end boundary */
if ( strcmp ( pos, "--" ) == 0 )
return NULL;
/* Skip trailing whitespace */
while ( ( *pos != '\n' ) && isspace ( *pos ) )
pos++;
/* Check for and skip terminating newline */
if ( *pos != '\n' ) {
DBGC ( image, "MIME %s malformed boundary marker\n",
image->name );
return NULL;
}
pos++;
return pos;
}
/**
* Extract part from multipart MIME image
*
* @v image MIME image
* @v headers MIME headers
* @v extracted Extracted image
* @ret rc Return status code
*
* Extraction will produce a MIME image containing only the selected
* part. (We rely on the recursive behaviour of image_extract_exec()
* to extract the content within the extracted part.)
*/
static int mime_extract_part ( struct image *image,
const struct mime_headers *headers,
struct image *extracted ) {
const struct mime_attribute *boundary = &headers->boundary;
const struct mime_type *type;
struct mime_headers subheaders;
const char *part;
const char *end;
size_t len;
int rc;
/* Find end of preamble */
end = mime_part_end ( image, boundary, image->text );
if ( ! end )
return -ENOENT;
/* Look for a usable part */
while ( ( part = mime_part_next ( image, boundary, end ) ) &&
( end = mime_part_end ( image, boundary, part ) ) ) {
/* Parse part headers */
DBGC ( image, "MIME %s found part at [%#zx,%#zx)\n",
image->name, ( part - image->text ),
( end - image->text ) );
if ( ( rc = mime_parse ( image, part, &subheaders ) ) != 0 )
return rc;
/* Check for a recognised MIME type */
type = mime_type ( subheaders.type );
if ( ! type )
continue;
DBGC ( image, "MIME %s found part with type %s\n",
image->name, type->name );
/* Extract part */
len = ( end - part );
if ( ( rc = image_set_len ( extracted, len ) ) != 0 )
return rc;
memcpy ( extracted->rwdata, part, len );
return 0;
}
DBGC ( image, "MIME %s found no usable part\n", image->name );
return -ENOENT;
}
/**
* Probe MIME image
*
* @v image Compressed kernel image
* @ret rc Return status code
*/
static int mime_probe ( struct image *image ) {
struct mime_headers headers;
int rc;
/* Check for MIME headers */
if ( ( rc = mime_parse ( image, image->text, &headers ) ) != 0 )
return rc;
return 0;
}
/** MIME types */
static const struct mime_type mime_types[] = {
{
.name = "multipart/mixed",
.extract = mime_extract_part,
},
{
.name = "text/x-ipxe",
.extract = mime_extract_entity,
},
};
/**
* Identify MIME type
*
* @v name Encoding
* @ret type MIME type, or NULL if not recognised
*/
static const struct mime_type * mime_type ( const char *name ) {
const struct mime_type *type;
size_t len;
unsigned int i;
/* Identify MIME type */
for ( i = 0 ; i < ( sizeof ( mime_types ) /
sizeof ( mime_types[0] ) ) ; i++ ) {
type = &mime_types[i];
len = strlen ( type->name );
if ( ( strncasecmp ( name, type->name, len ) == 0 ) &&
( ( name[len] == ';' ) || isspace ( name[len] ) ) ) {
return type;
}
}
return NULL;
}
/**
* Extract MIME image
*
* @v image Compressed kernel image
* @v extracted Extracted image
* @ret rc Return status code
*/
static int mime_extract ( struct image *image, struct image *extracted ) {
const struct mime_type *type;
struct mime_headers headers;
int rc;
/* Parse headers */
if ( ( rc = mime_parse ( image, image->text, &headers ) ) != 0 )
return rc;
/* Identify MIME type */
type = mime_type ( headers.type );
if ( ! type ) {
DBGC ( image, "MIME %s has no type\n", image->name );
return -ENOTSUP;
}
DBGC ( image, "MIME %s has type %s\n", image->name, type->name );
/* Extract image */
if ( ( rc = type->extract ( image, &headers, extracted ) ) != 0 )
return rc;
return 0;
}
/** MIME image type */
struct image_type mime_image_type __image_type ( PROBE_NORMAL ) = {
.name = "MIME",
.probe = mime_probe,
.extract = mime_extract,
.exec = image_extract_exec,
};