Commit 02229e2337a for php.net

commit 02229e2337aa6188d2eaa01d586b33a69fd39218
Author: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
Date:   Mon Aug 24 17:21:03 2026 +0200

    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

diff --git a/NEWS b/NEWS
index ccc3291c018..022d4897360 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 14cea21664c..ad76c2c2e29 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 1ab5d4858cb..1da93228f71 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 00000000000..0ab0a4b2126
--- /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 00000000000..13c2a3a7302
--- /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 00000000000..61a8f3f95f1
--- /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 00000000000..685d21c7c00
--- /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 '@'