[PHP-CVS] [php-src] master: standard: Add support for endianness modifier in pack()/unpack() format codes (#21074)

[email protected] (Alexandre Daubois via GitHub)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Alexandre Daubois (alexandre-daubois)
Committer: GitHub (web-flow)
Pusher: Girgias
Date: 2026-08-24T16:21:03+01:00

Commit: https://github.com/php/php-src/commit/02229e2337aa6188d2eaa01d586b33a69fd39218
Raw diff: https://github.com/php/php-src/commit/02229e2337aa6188d2eaa01d586b33a69fd39218.diff

standard: Add support for endianness modifier in pack()/unpack() format codes (#21074)

RFC (integer format codes): https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support
RFC (float format codes): https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier

Changed paths:
  A  ext/standard/tests/strings/pack_endian_modifiers.phpt
  A  ext/standard/tests/strings/pack_endian_modifiers_32.phpt
  A  ext/standard/tests/strings/pack_endian_modifiers_64.phpt
  A  ext/standard/tests/strings/pack_endian_modifiers_error.phpt
  M  NEWS
  M  UPGRADING
  M  ext/standard/pack.c


Diff:

diff --git a/NEWS b/NEWS
index ccc3291c018a..022d48973602 100644
--- a/NEWS
+++ b/NEWS
@@ -110,6 +110,8 @@ PHP                                                                        NEWS
 
 - Standard:
   . Fixed incorrect parameter name in convert_uudecode() warning. (lacatoire)
+  . Added support for the "<" and ">" endianness modifiers in pack() and
+    unpack() format codes. (alexandre-daubois)
 
 - Zip:
   . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive
diff --git a/UPGRADING b/UPGRADING
index 14cea21664c0..ad76c2c2e294 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -430,6 +430,14 @@ PHP 8.6 UPGRADE NOTES
     SNMP::setStringOutputFormat() methods. (eskyuu)
     RFC: https://wiki.php.net/rfc/snmp_improvements_2026#implement_more_mib_parsing_and_value_output_controls
 
+- Standard:
+  . pack() and unpack() now accept the "<" and ">" endianness modifiers on
+    the signed and unsigned integer format codes.
+    RFC: https://wiki.php.net/rfc/pack-unpack-endianness-signed-integers-support
+  . pack() and unpack() now accept the "<" and ">" endianness modifiers on
+    the float and double format codes.
+    RFC: https://wiki.php.net/rfc/pack-unpack-float-endianness-modifier
+
 - Streams:
   . Added new stream errors API including new classes, enums, functions and
     internal API. It is controlled using error_mode, error_store and
diff --git a/ext/standard/pack.c b/ext/standard/pack.c
index 1ab5d4858cbe..1da93228f71f 100644
--- a/ext/standard/pack.c
+++ b/ext/standard/pack.c
@@ -22,6 +22,7 @@
 	if ((a) < 0 || ((INT_MAX - outputpos)/((int)b)) < (a)) { \
 		efree(formatcodes);	\
 		efree(formatargs);	\
+		efree(formatendian);	\
 		zend_value_error("Type %c: integer overflow in format string", code); \
 		RETURN_THROWS(); \
 	} \
@@ -30,6 +31,7 @@
 typedef enum {
 	PHP_LITTLE_ENDIAN,
 	PHP_BIG_ENDIAN,
+	PHP_NO_ENDIAN_MODIFIER,
 } php_pack_endianness;
 
 #ifdef WORDS_BIGENDIAN
@@ -218,12 +220,27 @@ PHP_FUNCTION(pack)
 	/* We have a maximum of <formatlen> format codes to deal with */
 	formatcodes = safe_emalloc(formatlen, sizeof(*formatcodes), 0);
 	formatargs = safe_emalloc(formatlen, sizeof(*formatargs), 0);
+	php_pack_endianness *formatendian = safe_emalloc(formatlen, sizeof(*formatendian), 0);
 	currentarg = 0;
 
 	/* Preprocess format into formatcodes and formatargs */
 	for (i = 0; i < formatlen; formatcount++) {
 		char code = format[i++];
 		int arg = 1;
+		php_pack_endianness endian = PHP_NO_ENDIAN_MODIFIER;
+
+		/* Handle endianness modifier if any */
+		if (i < formatlen) {
+			char c = format[i];
+
+			if (c == '<') {
+				endian = PHP_LITTLE_ENDIAN;
+				i++;
+			} else if (c == '>') {
+				endian = PHP_BIG_ENDIAN;
+				i++;
+			}
+		}
 
 		/* Handle format arguments if any */
 		if (i < formatlen) {
@@ -248,6 +265,13 @@ PHP_FUNCTION(pack)
 			case 'x':
 			case 'X':
 			case '@':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					efree(formatcodes);
+					efree(formatargs);
+					efree(formatendian);
+					zend_value_error("Endianness modifier is not supported for format code '%c'", code);
+					RETURN_THROWS();
+				}
 				if (arg < 0) {
 					php_error_docref(NULL, E_WARNING, "Type %c: '*' ignored", code);
 					arg = 1;
@@ -260,9 +284,17 @@ PHP_FUNCTION(pack)
 			case 'Z':
 			case 'h':
 			case 'H':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					efree(formatcodes);
+					efree(formatargs);
+					efree(formatendian);
+					zend_value_error("Endianness modifier is not supported for format code '%c'", code);
+					RETURN_THROWS();
+				}
 				if (currentarg >= num_args) {
 					efree(formatcodes);
 					efree(formatargs);
+					efree(formatendian);
 					zend_value_error("Type %c: not enough arguments", code);
 					RETURN_THROWS();
 				}
@@ -271,6 +303,7 @@ PHP_FUNCTION(pack)
 					if (!try_convert_to_string(&argv[currentarg])) {
 						efree(formatcodes);
 						efree(formatargs);
+						efree(formatendian);
 						RETURN_THROWS();
 					}
 
@@ -286,35 +319,115 @@ PHP_FUNCTION(pack)
 				currentarg++;
 				break;
 
-			/* Use as many args as specified */
-			case 'q':
-			case 'Q':
+			/* 64-bit codes with explicit endianness, endianness modifiers not allowed */
 			case 'J':
 			case 'P':
-#if SIZEOF_ZEND_LONG < 8
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
 					efree(formatcodes);
 					efree(formatargs);
-					zend_value_error("64-bit format codes are not available for 32-bit versions of PHP");
+					efree(formatendian);
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code);
 					RETURN_THROWS();
+				}
+				ZEND_FALLTHROUGH;
+
+			/* 64-bit codes that support endianness modifiers */
+			case 'q':
+			case 'Q':
+#if SIZEOF_ZEND_LONG < 8
+				efree(formatcodes);
+				efree(formatargs);
+				efree(formatendian);
+				zend_value_error("64-bit format codes are not available for 32-bit versions of PHP");
+				RETURN_THROWS();
+#else
+				if (arg < 0) {
+					arg = num_args - currentarg;
+				}
+				if (currentarg > INT_MAX - arg) {
+					goto too_few_args;
+				}
+				currentarg += arg;
+
+				if (currentarg > num_args) {
+					goto too_few_args;
+				}
+				break;
 #endif
-			case 'c':
-			case 'C':
-			case 's':
-			case 'S':
-			case 'i':
-			case 'I':
-			case 'l':
-			case 'L':
+
+			/* Codes with explicit endianness, endianness modifiers not allowed */
 			case 'n':
 			case 'N':
 			case 'v':
 			case 'V':
-			case 'f': /* float */
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					efree(formatcodes);
+					efree(formatargs);
+					efree(formatendian);
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code);
+					RETURN_THROWS();
+				}
+				ZEND_FALLTHROUGH;
+
+			/* Codes that support endianness modifiers */
+			case 's':
+			case 'S':
+			case 'l':
+			case 'L':
+				if (arg < 0) {
+					arg = num_args - currentarg;
+				}
+				if (currentarg > INT_MAX - arg) {
+					goto too_few_args;
+				}
+				currentarg += arg;
+
+				if (currentarg > num_args) {
+					goto too_few_args;
+				}
+				break;
+
+			case 'c':
+			case 'C':
+			case 'i':
+			case 'I':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					efree(formatcodes);
+					efree(formatargs);
+					efree(formatendian);
+					zend_value_error("Endianness modifier is not supported for format code '%c'", code);
+					RETURN_THROWS();
+				}
+				if (arg < 0) {
+					arg = num_args - currentarg;
+				}
+				if (currentarg > INT_MAX - arg) {
+					goto too_few_args;
+				}
+				currentarg += arg;
+
+				if (currentarg > num_args) {
+					goto too_few_args;
+				}
+				break;
+
+			/* Codes with explicit endianness, endianness modifiers not allowed */
 			case 'g': /* little endian float */
 			case 'G': /* big endian float */
-			case 'd': /* double */
 			case 'e': /* little endian double */
 			case 'E': /* big endian double */
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					efree(formatcodes);
+					efree(formatargs);
+					efree(formatendian);
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', code);
+					RETURN_THROWS();
+				}
+				ZEND_FALLTHROUGH;
+
+			/* Codes that support endianness modifiers */
+			case 'f': /* float */
+			case 'd': /* double */
 				if (arg < 0) {
 					arg = num_args - currentarg;
 				}
@@ -327,6 +440,7 @@ PHP_FUNCTION(pack)
 too_few_args:
 					efree(formatcodes);
 					efree(formatargs);
+					efree(formatendian);
 					zend_value_error("Type %c: too few arguments", code);
 					RETURN_THROWS();
 				}
@@ -335,12 +449,14 @@ PHP_FUNCTION(pack)
 			default:
 				efree(formatcodes);
 				efree(formatargs);
+				efree(formatendian);
 				zend_value_error("Type %c: unknown format code", code);
 				RETURN_THROWS();
 		}
 
 		formatcodes[formatcount] = code;
 		formatargs[formatcount] = arg;
+		formatendian[formatcount] = endian;
 	}
 
 	if (currentarg < num_args) {
@@ -507,12 +623,16 @@ PHP_FUNCTION(pack)
 			case 'S':
 			case 'n':
 			case 'v': {
-				php_pack_endianness endianness = PHP_MACHINE_ENDIAN;
+				php_pack_endianness endianness;
 
 				if (code == 'n') {
 					endianness = PHP_BIG_ENDIAN;
 				} else if (code == 'v') {
 					endianness = PHP_LITTLE_ENDIAN;
+				} else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) {
+					endianness = formatendian[i];
+				} else {
+					endianness = PHP_MACHINE_ENDIAN;
 				}
 
 				while (arg-- > 0) {
@@ -534,12 +654,16 @@ PHP_FUNCTION(pack)
 			case 'L':
 			case 'N':
 			case 'V': {
-				php_pack_endianness endianness = PHP_MACHINE_ENDIAN;
+				php_pack_endianness endianness;
 
 				if (code == 'N') {
 					endianness = PHP_BIG_ENDIAN;
 				} else if (code == 'V') {
 					endianness = PHP_LITTLE_ENDIAN;
+				} else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) {
+					endianness = formatendian[i];
+				} else {
+					endianness = PHP_MACHINE_ENDIAN;
 				}
 
 				while (arg-- > 0) {
@@ -554,12 +678,16 @@ PHP_FUNCTION(pack)
 			case 'Q':
 			case 'J':
 			case 'P': {
-				php_pack_endianness endianness = PHP_MACHINE_ENDIAN;
+				php_pack_endianness endianness;
 
 				if (code == 'J') {
 					endianness = PHP_BIG_ENDIAN;
 				} else if (code == 'P') {
 					endianness = PHP_LITTLE_ENDIAN;
+				} else if (formatendian[i] != PHP_NO_ENDIAN_MODIFIER) {
+					endianness = formatendian[i];
+				} else {
+					endianness = PHP_MACHINE_ENDIAN;
 				}
 
 				while (arg-- > 0) {
@@ -570,59 +698,35 @@ PHP_FUNCTION(pack)
 			}
 #endif
 
-			case 'f': {
-				while (arg-- > 0) {
-					float v = (float) zval_get_double(&argv[currentarg++]);
-					memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v));
-					outputpos += sizeof(v);
-				}
-				break;
-			}
-
-			case 'g': {
-				/* pack little endian float */
-				while (arg-- > 0) {
-					float v = (float) zval_get_double(&argv[currentarg++]);
-					php_pack_copy_float(1, &ZSTR_VAL(output)[outputpos], v);
-					outputpos += sizeof(v);
-				}
-
-				break;
-			}
+			case 'f':
+			case 'g':
 			case 'G': {
-				/* pack big endian float */
 				while (arg-- > 0) {
 					float v = (float) zval_get_double(&argv[currentarg++]);
-					php_pack_copy_float(0, &ZSTR_VAL(output)[outputpos], v);
-					outputpos += sizeof(v);
-				}
-				break;
-			}
-
-			case 'd': {
-				while (arg-- > 0) {
-					double v = zval_get_double(&argv[currentarg++]);
-					memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v));
-					outputpos += sizeof(v);
-				}
-				break;
-			}
-
-			case 'e': {
-				/* pack little endian double */
-				while (arg-- > 0) {
-					double v = zval_get_double(&argv[currentarg++]);
-					php_pack_copy_double(1, &ZSTR_VAL(output)[outputpos], v);
+					if (code == 'g' || formatendian[i] == PHP_LITTLE_ENDIAN) {
+						php_pack_copy_float(1, &ZSTR_VAL(output)[outputpos], v);
+					} else if (code == 'G' || formatendian[i] == PHP_BIG_ENDIAN) {
+						php_pack_copy_float(0, &ZSTR_VAL(output)[outputpos], v);
+					} else {
+						memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v));
+					}
 					outputpos += sizeof(v);
 				}
 				break;
 			}
 
+			case 'd':
+			case 'e':
 			case 'E': {
-				/* pack big endian double */
 				while (arg-- > 0) {
 					double v = zval_get_double(&argv[currentarg++]);
-					php_pack_copy_double(0, &ZSTR_VAL(output)[outputpos], v);
+					if (code == 'e' || formatendian[i] == PHP_LITTLE_ENDIAN) {
+						php_pack_copy_double(1, &ZSTR_VAL(output)[outputpos], v);
+					} else if (code == 'E' || formatendian[i] == PHP_BIG_ENDIAN) {
+						php_pack_copy_double(0, &ZSTR_VAL(output)[outputpos], v);
+					} else {
+						memcpy(&ZSTR_VAL(output)[outputpos], &v, sizeof(v));
+					}
 					outputpos += sizeof(v);
 				}
 				break;
@@ -652,6 +756,7 @@ PHP_FUNCTION(pack)
 
 	efree(formatcodes);
 	efree(formatargs);
+	efree(formatendian);
 	ZSTR_VAL(output)[outputpos] = '\0';
 	ZSTR_LEN(output) = outputpos;
 	RETURN_NEW_STR(output);
@@ -710,6 +815,21 @@ PHP_FUNCTION(unpack)
 		char *name;
 		int namelen;
 		int size = 0;
+		php_pack_endianness endian = PHP_NO_ENDIAN_MODIFIER;
+
+		if (formatlen > 0) {
+			char c = *format;
+
+			if (c == '<') {
+				endian = PHP_LITTLE_ENDIAN;
+				format++;
+				formatlen--;
+			} else if (c == '>') {
+				endian = PHP_BIG_ENDIAN;
+				format++;
+				formatlen--;
+			}
+		}
 
 		/* Handle format arguments if any */
 		if (formatlen > 0) {
@@ -755,6 +875,10 @@ PHP_FUNCTION(unpack)
 		switch (type) {
 			/* Never use any input */
 			case 'X':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = -1;
 				if (repetitions < 0) {
 					php_error_docref(NULL, E_WARNING, "Type %c: '*' ignored", type);
@@ -763,18 +887,30 @@ PHP_FUNCTION(unpack)
 				break;
 
 			case '@':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = 0;
 				break;
 
 			case 'a':
 			case 'A':
 			case 'Z':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = repetitions;
 				repetitions = 1;
 				break;
 
 			case 'h':
 			case 'H':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = (repetitions > 0) ? ((unsigned int) repetitions + 1) / 2 : repetitions;
 				repetitions = 1;
 				break;
@@ -783,36 +919,73 @@ PHP_FUNCTION(unpack)
 			case 'c':
 			case 'C':
 			case 'x':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = 1;
 				break;
 
-			/* Use 2 bytes of input */
+			/* Use 2 bytes of input, endianness modifiers allowed */
 			case 's':
 			case 'S':
+				size = 2;
+				break;
+
+			/* Use 2 bytes of input with inherent endianness */
 			case 'n':
 			case 'v':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type);
+					RETURN_THROWS();
+				}
 				size = 2;
 				break;
 
 			/* Use sizeof(int) bytes of input */
 			case 'i':
 			case 'I':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier is not supported for format code '%c'", type);
+					RETURN_THROWS();
+				}
 				size = sizeof(int);
 				break;
 
-			/* Use 4 bytes of input */
+			/* Use 4 bytes of input, endianness modifiers allowed */
 			case 'l':
 			case 'L':
+				size = 4;
+				break;
+
+			/* Use 4 bytes of input with inherent endianness */
 			case 'N':
 			case 'V':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type);
+					RETURN_THROWS();
+				}
 				size = 4;
 				break;
 
-			/* Use 8 bytes of input */
+			/* Use 8 bytes of input, endianness modifiers allowed */
 			case 'q':
 			case 'Q':
+#if SIZEOF_ZEND_LONG > 4
+				size = 8;
+				break;
+#else
+				zend_value_error("64-bit format codes are not available for 32-bit versions of PHP");
+				RETURN_THROWS();
+#endif
+
+			/* Use 8 bytes of input with inherent endianness */
 			case 'J':
 			case 'P':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type);
+					RETURN_THROWS();
+				}
 #if SIZEOF_ZEND_LONG > 4
 				size = 8;
 				break;
@@ -821,17 +994,33 @@ PHP_FUNCTION(unpack)
 				RETURN_THROWS();
 #endif
 
-			/* Use sizeof(float) bytes of input */
+			/* Use sizeof(float) bytes of input, endianness modifiers allowed */
 			case 'f':
+				size = sizeof(float);
+				break;
+
+			/* Use sizeof(float) bytes of input with inherent endianness */
 			case 'g':
 			case 'G':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type);
+					RETURN_THROWS();
+				}
 				size = sizeof(float);
 				break;
 
-			/* Use sizeof(double) bytes of input */
+			/* Use sizeof(double) bytes of input, endianness modifiers allowed */
 			case 'd':
+				size = sizeof(double);
+				break;
+
+			/* Use sizeof(double) bytes of input with inherent endianness */
 			case 'e':
 			case 'E':
+				if (endian != PHP_NO_ENDIAN_MODIFIER) {
+					zend_value_error("Endianness modifier '%c' cannot be applied to format code '%c' which already has inherent endianness", (endian == PHP_LITTLE_ENDIAN) ? '<' : '>', type);
+					RETURN_THROWS();
+				}
 				size = sizeof(double);
 				break;
 
@@ -996,17 +1185,30 @@ PHP_FUNCTION(unpack)
 						break;
 					}
 
-					case 's':   /* signed machine endian   */
-					case 'S':   /* unsigned machine endian */
+					case 's':   /* signed, machine endian or explicit */
+					case 'S':   /* unsigned, machine endian or explicit */
 					case 'n':   /* unsigned big endian     */
 					case 'v': { /* unsigned little endian  */
 						zend_long v = 0;
 						uint16_t x = *((unaligned_uint16_t*) &input[inputpos]);
 
+						bool need_swap = false;
+						if (type == 'n') {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						} else if (type == 'v') {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_LITTLE_ENDIAN) {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_BIG_ENDIAN) {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						}
+
+						if (need_swap) {
+							x = php_pack_reverse_int16(x);
+						}
+
 						if (type == 's') {
 							v = (int16_t) x;
-						} else if ((type == 'n' && MACHINE_LITTLE_ENDIAN) || (type == 'v' && !MACHINE_LITTLE_ENDIAN)) {
-							v = php_pack_reverse_int16(x);
 						} else {
 							v = x;
 						}
@@ -1030,17 +1232,30 @@ PHP_FUNCTION(unpack)
 						break;
 					}
 
-					case 'l':   /* signed machine endian   */
-					case 'L':   /* unsigned machine endian */
+					case 'l':   /* signed, machine endian or explicit */
+					case 'L':   /* unsigned, machine endian or explicit */
 					case 'N':   /* unsigned big endian     */
 					case 'V': { /* unsigned little endian  */
 						zend_long v = 0;
 						uint32_t x = *((unaligned_uint32_t*) &input[inputpos]);
 
+						bool need_swap = false;
+						if (type == 'N') {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						} else if (type == 'V') {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_LITTLE_ENDIAN) {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_BIG_ENDIAN) {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						}
+
+						if (need_swap) {
+							x = php_pack_reverse_int32(x);
+						}
+
 						if (type == 'l') {
 							v = (int32_t) x;
-						} else if ((type == 'N' && MACHINE_LITTLE_ENDIAN) || (type == 'V' && !MACHINE_LITTLE_ENDIAN)) {
-							v = php_pack_reverse_int32(x);
 						} else {
 							v = x;
 						}
@@ -1050,17 +1265,30 @@ PHP_FUNCTION(unpack)
 					}
 
 #if SIZEOF_ZEND_LONG > 4
-					case 'q':   /* signed machine endian   */
-					case 'Q':   /* unsigned machine endian */
+					case 'q':   /* signed, machine endian or explicit */
+					case 'Q':   /* unsigned, machine endian or explicit */
 					case 'J':   /* unsigned big endian     */
 					case 'P': { /* unsigned little endian  */
 						zend_long v = 0;
 						uint64_t x = *((unaligned_uint64_t*) &input[inputpos]);
 
+						bool need_swap = false;
+						if (type == 'J') {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						} else if (type == 'P') {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_LITTLE_ENDIAN) {
+							need_swap = !MACHINE_LITTLE_ENDIAN;
+						} else if (endian == PHP_BIG_ENDIAN) {
+							need_swap = MACHINE_LITTLE_ENDIAN;
+						}
+
+						if (need_swap) {
+							x = php_pack_reverse_int64(x);
+						}
+
 						if (type == 'q') {
 							v = (int64_t) x;
-						} else if ((type == 'J' && MACHINE_LITTLE_ENDIAN) || (type == 'P' && !MACHINE_LITTLE_ENDIAN)) {
-							v = php_pack_reverse_int64(x);
 						} else {
 							v = x;
 						}
@@ -1076,9 +1304,9 @@ PHP_FUNCTION(unpack)
 					{
 						float v;
 
-						if (type == 'g') {
+						if (type == 'g' || endian == PHP_LITTLE_ENDIAN) {
 							v = php_pack_parse_float(1, &input[inputpos]);
-						} else if (type == 'G') {
+						} else if (type == 'G' || endian == PHP_BIG_ENDIAN) {
 							v = php_pack_parse_float(0, &input[inputpos]);
 						} else {
 							memcpy(&v, &input[inputpos], sizeof(float));
@@ -1094,9 +1322,9 @@ PHP_FUNCTION(unpack)
 					case 'E': /* big endian float */
 					{
 						double v;
-						if (type == 'e') {
+						if (type == 'e' || endian == PHP_LITTLE_ENDIAN) {
 							v = php_pack_parse_double(1, &input[inputpos]);
-						} else if (type == 'E') {
+						} else if (type == 'E' || endian == PHP_BIG_ENDIAN) {
 							v = php_pack_parse_double(0, &input[inputpos]);
 						} else {
 							memcpy(&v, &input[inputpos], sizeof(double));
diff --git a/ext/standard/tests/strings/pack_endian_modifiers.phpt b/ext/standard/tests/strings/pack_endian_modifiers.phpt
new file mode 100644
index 000000000000..0ab0a4b2126c
--- /dev/null
+++ b/ext/standard/tests/strings/pack_endian_modifiers.phpt
@@ -0,0 +1,182 @@
+--TEST--
+pack()/unpack() endianness modifiers
+--FILE--
+<?php
+
+var_dump(bin2hex(pack("s<", 0x0102)));
+var_dump(bin2hex(pack("S<", 0x0102)));
+
+var_dump(bin2hex(pack("s>", 0x0102)));
+var_dump(bin2hex(pack("S>", 0x0102)));
+
+var_dump(pack("s<", 0x0102) === pack("v", 0x0102));
+var_dump(pack("S<", 0x0102) === pack("v", 0x0102));
+
+var_dump(pack("s>", 0x0102) === pack("n", 0x0102));
+var_dump(pack("S>", 0x0102) === pack("n", 0x0102));
+
+var_dump(bin2hex(pack("l<", 0x01020304)));
+var_dump(bin2hex(pack("L<", 0x01020304)));
+
+var_dump(bin2hex(pack("l>", 0x01020304)));
+var_dump(bin2hex(pack("L>", 0x01020304)));
+
+var_dump(pack("l<", 0x01020304) === pack("V", 0x01020304));
+var_dump(pack("L<", 0x01020304) === pack("V", 0x01020304));
+
+var_dump(pack("l>", 0x01020304) === pack("N", 0x01020304));
+var_dump(pack("L>", 0x01020304) === pack("N", 0x01020304));
+
+// === Integer unpack with endianness modifiers ===
+
+var_dump(unpack("s<", "\x02\x01"));
+var_dump(unpack("S<", "\x02\x01"));
+
+var_dump(unpack("s>", "\x01\x02"));
+var_dump(unpack("S>", "\x01\x02"));
+
+var_dump(unpack("s<", "\xfe\xff"));  // -2 in little-endian
+var_dump(unpack("s>", "\xff\xfe"));  // -2 in big-endian
+
+var_dump(unpack("l<", "\x04\x03\x02\x01"));
+var_dump(unpack("L<", "\x04\x03\x02\x01"));
+
+var_dump(unpack("l>", "\x01\x02\x03\x04"));
+var_dump(unpack("L>", "\x01\x02\x03\x04"));
+
+var_dump(unpack("l<", "\xfe\xff\xff\xff"));  // -2 in little-endian
+var_dump(unpack("l>", "\xff\xff\xff\xfe"));  // -2 in big-endian
+
+var_dump(bin2hex(pack("s<2", 0x0102, 0x0304)));
+var_dump(bin2hex(pack("s>2", 0x0102, 0x0304)));
+
+var_dump(unpack("s<2", "\x02\x01\x04\x03"));
+var_dump(unpack("s>2", "\x01\x02\x03\x04"));
+
+var_dump(unpack("s<value/L>count", "\x02\x01\x00\x00\x00\x05"));
+
+var_dump(pack("f<", 3.14) === pack("g", 3.14));
+var_dump(pack("f>", 3.14) === pack("G", 3.14));
+
+var_dump(pack("d<", 3.14) === pack("e", 3.14));
+var_dump(pack("d>", 3.14) === pack("E", 3.14));
+
+$packed_le = pack("g", 3.14);
+$packed_be = pack("G", 3.14);
+$unpacked_le = unpack("f<", $packed_le);
+$unpacked_be = unpack("f>", $packed_be);
+$unpacked_g = unpack("g", $packed_le);
+$unpacked_G = unpack("G", $packed_be);
+var_dump($unpacked_le[1] === $unpacked_g[1]);
+var_dump($unpacked_be[1] === $unpacked_G[1]);
+
+$packed_le = pack("e", 3.14);
+$packed_be = pack("E", 3.14);
+$unpacked_le = unpack("d<", $packed_le);
+$unpacked_be = unpack("d>", $packed_be);
+$unpacked_e = unpack("e", $packed_le);
+$unpacked_E = unpack("E", $packed_be);
+var_dump($unpacked_le[1] === $unpacked_e[1]);
+var_dump($unpacked_be[1] === $unpacked_E[1]);
+
+$machine_float = pack("f", 1.5);
+var_dump(unpack("f", $machine_float)[1] === 1.5);
+
+$machine_double = pack("d", 1.5);
+var_dump(unpack("d", $machine_double)[1] === 1.5);
+?>
+--EXPECT--
+string(4) "0201"
+string(4) "0201"
+string(4) "0102"
+string(4) "0102"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(8) "04030201"
+string(8) "04030201"
+string(8) "01020304"
+string(8) "01020304"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+array(1) {
+  [1]=>
+  int(258)
+}
+array(1) {
+  [1]=>
+  int(258)
+}
+array(1) {
+  [1]=>
+  int(258)
+}
+array(1) {
+  [1]=>
+  int(258)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
+array(1) {
+  [1]=>
+  int(16909060)
+}
+array(1) {
+  [1]=>
+  int(16909060)
+}
+array(1) {
+  [1]=>
+  int(16909060)
+}
+array(1) {
+  [1]=>
+  int(16909060)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
+string(8) "02010403"
+string(8) "01020304"
+array(2) {
+  [1]=>
+  int(258)
+  [2]=>
+  int(772)
+}
+array(2) {
+  [1]=>
+  int(258)
+  [2]=>
+  int(772)
+}
+array(2) {
+  ["value"]=>
+  int(258)
+  ["count"]=>
+  int(5)
+}
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/standard/tests/strings/pack_endian_modifiers_32.phpt b/ext/standard/tests/strings/pack_endian_modifiers_32.phpt
new file mode 100644
index 000000000000..13c2a3a73025
--- /dev/null
+++ b/ext/standard/tests/strings/pack_endian_modifiers_32.phpt
@@ -0,0 +1,37 @@
+--TEST--
+pack()/unpack() endianness modifiers on 64-bit format codes, 32-bit systems
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE > 4) die("skip 32bit test only");
+?>
+--FILE--
+<?php
+
+$formats = ['q<', 'q>', 'Q<', 'Q>'];
+foreach ($formats as $fmt) {
+    try {
+        pack($fmt, 0);
+        echo "FAIL: Expected ValueError for pack('$fmt', 0)\n";
+    } catch (ValueError $e) {
+        echo "pack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+foreach ($formats as $fmt) {
+    try {
+        unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00");
+        echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n";
+    } catch (ValueError $e) {
+        echo "unpack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+?>
+--EXPECT--
+pack('q<'): 64-bit format codes are not available for 32-bit versions of PHP
+pack('q>'): 64-bit format codes are not available for 32-bit versions of PHP
+pack('Q<'): 64-bit format codes are not available for 32-bit versions of PHP
+pack('Q>'): 64-bit format codes are not available for 32-bit versions of PHP
+unpack('q<'): 64-bit format codes are not available for 32-bit versions of PHP
+unpack('q>'): 64-bit format codes are not available for 32-bit versions of PHP
+unpack('Q<'): 64-bit format codes are not available for 32-bit versions of PHP
+unpack('Q>'): 64-bit format codes are not available for 32-bit versions of PHP
diff --git a/ext/standard/tests/strings/pack_endian_modifiers_64.phpt b/ext/standard/tests/strings/pack_endian_modifiers_64.phpt
new file mode 100644
index 000000000000..61a8f3f95f1c
--- /dev/null
+++ b/ext/standard/tests/strings/pack_endian_modifiers_64.phpt
@@ -0,0 +1,63 @@
+--TEST--
+pack()/unpack() endianness modifiers on 64-bit format codes
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE < 8) die("skip 64bit test only");
+?>
+--FILE--
+<?php
+
+var_dump(bin2hex(pack("q<", 0x0102030405060708)));
+var_dump(bin2hex(pack("Q<", 0x0102030405060708)));
+
+var_dump(bin2hex(pack("q>", 0x0102030405060708)));
+var_dump(bin2hex(pack("Q>", 0x0102030405060708)));
+
+var_dump(pack("q<", 0x0102030405060708) === pack("P", 0x0102030405060708));
+var_dump(pack("Q<", 0x0102030405060708) === pack("P", 0x0102030405060708));
+
+var_dump(pack("q>", 0x0102030405060708) === pack("J", 0x0102030405060708));
+var_dump(pack("Q>", 0x0102030405060708) === pack("J", 0x0102030405060708));
+
+var_dump(unpack("q<", "\x08\x07\x06\x05\x04\x03\x02\x01"));
+var_dump(unpack("Q<", "\x08\x07\x06\x05\x04\x03\x02\x01"));
+
+var_dump(unpack("q>", "\x01\x02\x03\x04\x05\x06\x07\x08"));
+var_dump(unpack("Q>", "\x01\x02\x03\x04\x05\x06\x07\x08"));
+
+var_dump(unpack("q<", "\xfe\xff\xff\xff\xff\xff\xff\xff"));  // -2 in little-endian
+var_dump(unpack("q>", "\xff\xff\xff\xff\xff\xff\xff\xfe"));  // -2 in big-endian
+?>
+--EXPECT--
+string(16) "0807060504030201"
+string(16) "0807060504030201"
+string(16) "0102030405060708"
+string(16) "0102030405060708"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+array(1) {
+  [1]=>
+  int(72623859790382856)
+}
+array(1) {
+  [1]=>
+  int(72623859790382856)
+}
+array(1) {
+  [1]=>
+  int(72623859790382856)
+}
+array(1) {
+  [1]=>
+  int(72623859790382856)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
+array(1) {
+  [1]=>
+  int(-2)
+}
diff --git a/ext/standard/tests/strings/pack_endian_modifiers_error.phpt b/ext/standard/tests/strings/pack_endian_modifiers_error.phpt
new file mode 100644
index 000000000000..685d21c7c00e
--- /dev/null
+++ b/ext/standard/tests/strings/pack_endian_modifiers_error.phpt
@@ -0,0 +1,105 @@
+--TEST--
+pack()/unpack() endianness modifiers, invalid combinations
+--FILE--
+<?php
+
+$inherent_formats = ['n<', 'v>', 'N<', 'V>', 'J<', 'P>'];
+foreach ($inherent_formats as $fmt) {
+    try {
+        pack($fmt, 1);
+        echo "FAIL: Expected ValueError for pack('$fmt', 1)\n";
+    } catch (ValueError $e) {
+        echo "pack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+$inherent_float_formats = ['g<', 'G>', 'e<', 'E>'];
+foreach ($inherent_float_formats as $fmt) {
+    try {
+        pack($fmt, 1.0);
+        echo "FAIL: Expected ValueError for pack('$fmt', 1.0)\n";
+    } catch (ValueError $e) {
+        echo "pack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+$unsupported_formats = ['c<', 'C>', 'a<', 'A>', 'h<', 'H>', 'i<', 'I>', 'x<', 'X>', '@<'];
+foreach ($unsupported_formats as $fmt) {
+    try {
+        pack($fmt, 1);
+        echo "FAIL: Expected ValueError for pack('$fmt', 1)\n";
+    } catch (ValueError $e) {
+        echo "pack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+foreach (['n<', 'v>', 'N<', 'V>', 'J<', 'P>'] as $fmt) {
+    try {
+        unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00");
+        echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n";
+    } catch (ValueError $e) {
+        echo "unpack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+foreach (['g<', 'G>', 'e<', 'E>'] as $fmt) {
+    try {
+        unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00");
+        echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n";
+    } catch (ValueError $e) {
+        echo "unpack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+
+foreach (['c<', 'C>', 'a<', 'A>', 'h<', 'H>', 'i<', 'I>', 'x<', 'X>', '@<'] as $fmt) {
+    try {
+        unpack($fmt, "\x00\x00\x00\x00\x00\x00\x00\x00");
+        echo "FAIL: Expected ValueError for unpack('$fmt', ...)\n";
+    } catch (ValueError $e) {
+        echo "unpack('$fmt'): " . $e->getMessage() . "\n";
+    }
+}
+?>
+--EXPECT--
+pack('n<'): Endianness modifier '<' cannot be applied to format code 'n' which already has inherent endianness
+pack('v>'): Endianness modifier '>' cannot be applied to format code 'v' which already has inherent endianness
+pack('N<'): Endianness modifier '<' cannot be applied to format code 'N' which already has inherent endianness
+pack('V>'): Endianness modifier '>' cannot be applied to format code 'V' which already has inherent endianness
+pack('J<'): Endianness modifier '<' cannot be applied to format code 'J' which already has inherent endianness
+pack('P>'): Endianness modifier '>' cannot be applied to format code 'P' which already has inherent endianness
+pack('g<'): Endianness modifier '<' cannot be applied to format code 'g' which already has inherent endianness
+pack('G>'): Endianness modifier '>' cannot be applied to format code 'G' which already has inherent endianness
+pack('e<'): Endianness modifier '<' cannot be applied to format code 'e' which already has inherent endianness
+pack('E>'): Endianness modifier '>' cannot be applied to format code 'E' which already has inherent endianness
+pack('c<'): Endianness modifier is not supported for format code 'c'
+pack('C>'): Endianness modifier is not supported for format code 'C'
+pack('a<'): Endianness modifier is not supported for format code 'a'
+pack('A>'): Endianness modifier is not supported for format code 'A'
+pack('h<'): Endianness modifier is not supported for format code 'h'
+pack('H>'): Endianness modifier is not supported for format code 'H'
+pack('i<'): Endianness modifier is not supported for format code 'i'
+pack('I>'): Endianness modifier is not supported for format code 'I'
+pack('x<'): Endianness modifier is not supported for format code 'x'
+pack('X>'): Endianness modifier is not supported for format code 'X'
+pack('@<'): Endianness modifier is not supported for format code '@'
+unpack('n<'): Endianness modifier '<' cannot be applied to format code 'n' which already has inherent endianness
+unpack('v>'): Endianness modifier '>' cannot be applied to format code 'v' which already has inherent endianness
+unpack('N<'): Endianness modifier '<' cannot be applied to format code 'N' which already has inherent endianness
+unpack('V>'): Endianness modifier '>' cannot be applied to format code 'V' which already has inherent endianness
+unpack('J<'): Endianness modifier '<' cannot be applied to format code 'J' which already has inherent endianness
+unpack('P>'): Endianness modifier '>' cannot be applied to format code 'P' which already has inherent endianness
+unpack('g<'): Endianness modifier '<' cannot be applied to format code 'g' which already has inherent endianness
+unpack('G>'): Endianness modifier '>' cannot be applied to format code 'G' which already has inherent endianness
+unpack('e<'): Endianness modifier '<' cannot be applied to format code 'e' which already has inherent endianness
+unpack('E>'): Endianness modifier '>' cannot be applied to format code 'E' which already has inherent endianness
+unpack('c<'): Endianness modifier is not supported for format code 'c'
+unpack('C>'): Endianness modifier is not supported for format code 'C'
+unpack('a<'): Endianness modifier is not supported for format code 'a'
+unpack('A>'): Endianness modifier is not supported for format code 'A'
+unpack('h<'): Endianness modifier is not supported for format code 'h'
+unpack('H>'): Endianness modifier is not supported for format code 'H'
+unpack('i<'): Endianness modifier is not supported for format code 'i'
+unpack('I>'): Endianness modifier is not supported for format code 'I'
+unpack('x<'): Endianness modifier is not supported for format code 'x'
+unpack('X>'): Endianness modifier is not supported for format code 'X'
+unpack('@<'): Endianness modifier is not supported for format code '@'
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.