Merge pull request #22247 from hrydgard/kl4e

Implement KL4E/KL3E decompression, so firmware modules that use it can load
This commit is contained in:
Henrik Rydgård
2026-09-07 12:22:16 -06:00
committed by GitHub
10 changed files with 500 additions and 13 deletions
+2
View File
@@ -693,6 +693,8 @@ add_library(Core STATIC
Util/BlockAllocator.h
Util/PPGeDraw.cpp
Util/PPGeDraw.h
Util/KL4E.cpp
Util/KL4E.h
Util/PSARUnpack.cpp
Util/PSARUnpack.h
Util/RecentFiles.cpp
+2
View File
@@ -921,6 +921,7 @@
<ClCompile Include="Util\PathUtil.cpp" />
<ClCompile Include="Util\PortManager.cpp" />
<ClCompile Include="Util\PPGeDraw.cpp" />
<ClCompile Include="Util\KL4E.cpp" />
<ClCompile Include="Util\PSARUnpack.cpp" />
<ClCompile Include="..\ext\xxhash.c">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">MaxSpeed</Optimization>
@@ -1289,6 +1290,7 @@
<ClInclude Include="Util\PathUtil.h" />
<ClInclude Include="Util\PortManager.h" />
<ClInclude Include="Util\PPGeDraw.h" />
<ClInclude Include="Util\KL4E.h" />
<ClInclude Include="Util\PSARUnpack.h" />
<ClInclude Include="..\ext\xxhash.h" />
<ClInclude Include="Util\RecentFiles.h" />
+3
View File
@@ -363,6 +363,9 @@
<ClCompile Include="Util\PPGeDraw.cpp">
<Filter>Util</Filter>
</ClCompile>
<ClCompile Include="Util\KL4E.cpp">
<Filter>Util</Filter>
</ClCompile>
<ClCompile Include="Util\PSARUnpack.cpp">
<Filter>Util</Filter>
</ClCompile>
+28 -13
View File
@@ -48,6 +48,7 @@
#include "Core/ELF/ElfReader.h"
#include "Core/ELF/PBPReader.h"
#include "Core/ELF/PrxDecrypter.h"
#include "Core/Util/KL4E.h"
#include "Core/FileSystems/FileSystem.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Util/BlockAllocator.h"
@@ -1273,7 +1274,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
g_OSD.Show(OSDType::MESSAGE_WARNING, StringFromFormat("HLE for '%s' has been manually disabled", head->modname));
}
const u8 *in = ptr;
const auto isGzip = head->comp_attribute & 1;
const bool isCompressed = (head->comp_attribute & 1) != 0;
// Kind of odd.
u32 size = head->psp_size;
if (size > elfSize) {
@@ -1311,22 +1312,36 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
return nullptr;
}
// decompress if required.
if (isGzip) {
_dbg_assert_(Read32(ptr + 0x150) != ELF_MAGIC);
// decompress if required. comp_attribute bit 0 says "compressed"; which scheme is then
// decided by the payload's own magic - gzip, or Sony's KL4E/KL3E. (JPCSP instead reads
// bits 8-11 of comp_attribute, but the magic is right there and can't disagree.)
if (isCompressed) {
// Can't decompress in place so we need a temporary buffer.
u8 *temp = (u8 *)malloc(decryptedSize);
_assert_msg_(temp != nullptr, "Failed to allocate gzip decompression buffer (decryptedSize: %d)", decryptedSize);
_assert_msg_(temp != nullptr, "Failed to allocate decompression buffer (decryptedSize: %d)", decryptedSize);
memcpy(temp, ptr, decryptedSize);
int outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp);
bool isKL3E = false;
int outBytes;
const char *scheme;
if (IsKL4EMagic(temp, decryptedSize, &isKL3E)) {
scheme = isKL3E ? "KL3E" : "KL4E";
// The decompressor is handed the stream header, i.e. past the four-byte magic.
outBytes = DecompressKL4E((u8 *)ptr, maxElfSize, temp + 4, (size_t)decryptedSize - 4, nullptr, isKL3E);
if (outBytes < 0) {
// A PSP error code, not a byte count.
outBytes = -1;
}
} else {
scheme = "gzip";
_dbg_assert_(Read32(ptr + 0x150) != ELF_MAGIC);
outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp);
}
free(temp);
if (outBytes < 0) {
// Not necessarily actually gzip - some kd/ system modules (and possibly VSH
// modules) use KL4E compression instead, which we don't support decompressing.
// Bail out cleanly here rather than falling through to parse whatever's left
// in the buffer (still compressed, not a valid ELF) as if it were real code.
*error_string = StringFromFormat("Module '%s' decompression failed", head->modname);
// Bail out cleanly rather than falling through to parse whatever's left in the
// buffer (still compressed, not a valid ELF) as if it were real code.
*error_string = StringFromFormat("Module '%s' %s decompression failed", head->modname, scheme);
delete [] newptr;
module->Cleanup();
kernelObjects.Destroy<PSPModule>(module->GetUID());
@@ -1334,7 +1349,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
error = SCE_KERNEL_ERROR_FILEERR;
return nullptr;
}
INFO_LOG(Log::sceModule, "gzip is enabled in '%s', decompressing (%d -> %d bytes, bufmax=%d).", head->modname, decryptedSize, outBytes, maxElfSize);
INFO_LOG(Log::sceModule, "'%s' is %s-compressed, decompressing (%d -> %d bytes, bufmax=%d).", head->modname, scheme, decryptedSize, outBytes, maxElfSize);
}
if (fakeLoadedModule) {
+419
View File
@@ -0,0 +1,419 @@
// Copyright (c) 2026- PPSSPP Project.
// 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, version 2.0 or later versions.
// 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// KL4E / KL3E decompression. See docs/KL4E.md.
//
// LZ77 tokens - "emit this literal" or "repeat N bytes from M back" - where every bit is coded
// with an adaptive binary arithmetic coder rather than written directly. Structurally close to
// LZMA. KL3E differs from KL4E in exactly one constant (kPowLimitLong).
//
// Written from the format description, not ported from existing code.
#include <cstring>
#include "Common/Log.h"
#include "Core/HLE/ErrorCodes.h"
#include "Core/Util/KL4E.h"
namespace {
// Adaptation profiles. A probability is the chance the next bit is a 1, out of 256: on a 0 it
// just decays, on a 1 it decays and then gains the bonus back, so it climbs toward a ceiling of
// roughly (bonus << decay).
const int kDecayNormal = 3, kBonusNormal = 31;
const int kDecayFlag = 4, kBonusFlag = 15;
// Table sizes. Each is exactly large enough for the highest index its indexing scheme can
// produce - except copyDistProbs, see the bounds check in the distance decode.
const int kLitProbsSize = 2040; // 8 literal contexts * 255
const int kCopyCountBitsProbsSize = 64; // 8 states * 8 unary steps
const int kCopyCountProbsSize = 256;
const int kCopyDistBitsProbsSize = 304;
const int kCopyDistProbsSize = 144;
// The doubling walk that sizes a distance code stops once it reaches this. The single constant
// that separates the two formats.
const int kPowLimitKL4E = 256;
const int kPowLimitKL3E = 128;
// Short matches use a tighter limit in both formats.
const int kPowLimitShort = 64;
struct ArithDecoder {
const u8 *in;
const u8 *inEnd;
u32 range;
u32 code;
bool overrun;
u8 NextByte() {
if (in >= inEnd) {
// The format carries no length and relies on the stream terminating itself, so a
// truncated file would otherwise read forever. Feed zeroes and remember.
overrun = true;
return 0;
}
return *in++;
}
// One bit against an adaptive probability.
int ReadBit(u8 *prob, int decay, int bonus) {
u32 bound;
const u32 p = *prob;
if ((range >> 24) == 0) {
// Renormalize. The bound uses the range from *before* the shift.
code = (code << 8) + NextByte();
bound = range * p;
range <<= 8;
} else {
bound = (range >> 8) * p;
}
const u32 decayed = p - (p >> decay);
if (code >= bound) {
code -= bound;
range -= bound;
*prob = (u8)decayed;
return 0;
}
range = bound;
*prob = (u8)(decayed + bonus);
return 1;
}
// A bit the encoder judged incompressible: no probability, no adaptation.
int ReadBitUniform() {
if ((range >> 24) == 0) {
code = (code << 8) + NextByte();
range <<= 7;
} else {
range >>= 1;
}
if (code >= range) {
code -= range;
return 0;
}
return 1;
}
// Same, skipping the refill check. Only valid straight after a read that did refill, which
// is how runs of uniform bits are emitted.
int ReadBitUniformNoNorm() {
range >>= 1;
if (code >= range) {
code -= range;
return 0;
}
return 1;
}
// Eight bits into an accumulator that starts at 1, so the count is implicit. The context is
// a mix of the previous byte and the output position's alignment; `shift` slides the window
// between the two, the same choice LZMA's lc makes.
u32 ReadLiteral(u8 *litProbs, int outPos, u32 prevByte, int shift) {
const u32 ctx = ((u32)(outPos & 7) << 8) | (prevByte & 0xFF);
u8 *probs = litProbs + ((ctx >> shift) & 7) * 255 - 1;
u32 acc = 1;
while (acc < 0x100) {
acc = (acc << 1) | ReadBit(&probs[acc], kDecayNormal, kBonusNormal);
}
return acc & 0xFF;
}
};
} // namespace
bool IsKL4EMagic(const u8 *data, size_t size, bool *isKL3E) {
if (size < 4) {
return false;
}
if (!memcmp(data, "KL4E", 4)) {
if (isKL3E) {
*isKL3E = false;
}
return true;
}
if (!memcmp(data, "KL3E", 4)) {
if (isKL3E) {
*isKL3E = true;
}
return true;
}
return false;
}
int DecompressKL4E(u8 *out, int outSize, const u8 *in, size_t inSize, const u8 **end, bool isKL3E) {
if (!out || !in || outSize <= 0 || inSize < 5) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
// --- five-byte stream header ---
const u8 flags = in[0];
// Big-endian, which almost nothing else on the PSP is.
const u32 headerWord = ((u32)in[1] << 24) | ((u32)in[2] << 16) | ((u32)in[3] << 8) | in[4];
if (flags & 0x80) {
// Stored, not compressed: the header word is a length and the bytes follow verbatim.
// A payload that exactly fills the output buffer is rejected, not accepted.
if (headerWord >= (u32)outSize) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
if (5 + (size_t)headerWord > inSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
memcpy(out, in + 5, headerWord);
if (end) {
*end = in + 5 + headerWord;
}
return (int)headerWord;
}
// bits 4-3 pick the starting probability, bits 2-0 the literal context selector.
const u8 seed = (u8)(0x80 - (((flags >> 3) & 3) << 4));
const int shift = flags & 7;
u8 litProbs[kLitProbsSize];
u8 copyCountBitsProbs[kCopyCountBitsProbsSize];
u8 copyCountProbs[kCopyCountProbsSize];
u8 copyDistBitsProbs[kCopyDistBitsProbsSize];
u8 copyDistProbs[kCopyDistProbsSize];
memset(litProbs, seed, sizeof(litProbs));
memset(copyCountBitsProbs, seed, sizeof(copyCountBitsProbs));
memset(copyCountProbs, seed, sizeof(copyCountProbs));
memset(copyDistBitsProbs, seed, sizeof(copyDistBitsProbs));
memset(copyDistProbs, seed, sizeof(copyDistProbs));
ArithDecoder dec;
dec.in = in + 5;
dec.inEnd = in + inSize;
dec.range = 0xFFFFFFFF;
dec.code = headerWord;
dec.overrun = false;
const int powLimitLong = isKL3E ? kPowLimitKL3E : kPowLimitKL4E;
// outPos indexes the byte being produced. The loop head advances it, which is why a match
// only advances it by copyCount after writing copyCount + 1 bytes.
int outPos = 0;
u32 prevByte = 0;
// An index into copyCountBitsProbs that persists across tokens - the "state". The unary walk
// below moves it in steps of 8 and does not put it back, so its low three bits are what the
// length code's context uses.
int countBitsIdx = 0;
// The first literal has no flag bit in front of it.
prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
out[0] = (u8)prevByte;
while (true) {
outPos++;
if (dec.overrun) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
if (dec.ReadBit(&copyCountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag) == 0) {
// Literal.
countBitsIdx = countBitsIdx > 0 ? countBitsIdx - 1 : 0;
if (outPos >= outSize) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
prevByte = dec.ReadLiteral(litProbs, outPos, prevByte, shift);
out[outPos] = (u8)prevByte;
continue;
}
// A match. First, how many bits the length code uses, as a unary run stepping the index
// by 8 so the low three bits stay put.
u32 copyCount = 1;
int copyCountBits = -1;
while (copyCountBits < 6) {
countBitsIdx += 8;
if (countBitsIdx >= kCopyCountBitsProbsSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
if (!dec.ReadBit(&copyCountBitsProbs[countBitsIdx], kDecayFlag, kBonusFlag)) {
break;
}
copyCountBits++;
}
// The length, and with it the distance code's parameters: the bit that lands in the
// length's low position also selects them, for short matches.
int powLimit = kPowLimitShort;
int distBase = copyCountBits;
if (copyCountBits >= 0) {
const int offset = (copyCountBits << 5)
| (((outPos & 3) << (copyCountBits + 3)) & 0x18)
| (countBitsIdx & 7);
u8 *probs = copyCountProbs + offset;
// High bits: two adaptive, then uniform for anything longer.
if (copyCountBits >= 3) {
copyCount = 2 + dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
if (copyCountBits > 3) {
copyCount = (copyCount << 1) | dec.ReadBit(probs + 24, kDecayNormal, kBonusNormal);
if (copyCountBits > 4) {
copyCount = (copyCount << 1) | dec.ReadBitUniform();
}
for (int i = 5; i < copyCountBits; i++) {
copyCount = (copyCount << 1) | dec.ReadBitUniformNoNorm();
}
}
}
copyCount <<= 1;
if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
copyCount |= 1;
if (copyCountBits <= 0) {
powLimit = powLimitLong;
distBase = 56 + copyCountBits;
}
} else if (copyCountBits <= 0) {
powLimit = kPowLimitShort;
distBase = copyCountBits;
}
if (copyCountBits > 0) {
copyCount = (copyCount << 1) | dec.ReadBit(probs + 8, kDecayNormal, kBonusNormal);
if (copyCountBits != 1) {
copyCount <<= 1;
if (dec.ReadBit(probs + 16, kDecayNormal, kBonusNormal)) {
copyCount++;
// 0xFF ends the stream. There is no length in the header and no
// terminator byte; this is the only normal way out of the loop.
if (copyCount == 0xFF) {
if (end) {
*end = dec.in;
}
return outPos;
}
}
}
powLimit = powLimitLong;
distBase = 56 + copyCountBits;
}
}
// How many bits the distance code uses, by a doubling walk. curPow only picks up the
// extra 8 when the walk continues, which is what makes the deepest reachable index fit
// copyDistBitsProbs exactly.
u32 copyDist = 0;
bool distanceIsZero = false;
int curPow = 8;
int copyDistBits = 0;
while (true) {
const int probIndex = distBase + curPow - 7;
if (probIndex < 0 || probIndex >= kCopyDistBitsProbsSize) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
u8 *prob = &copyDistBitsProbs[probIndex];
curPow <<= 1;
copyDistBits = curPow - powLimit;
if (!dec.ReadBit(prob, kDecayNormal, kBonusNormal)) {
if (copyDistBits >= 0) {
if (copyDistBits != 0) {
copyDistBits -= 8;
break;
}
// No distance bits at all: a match against the byte just emitted, i.e. a run.
distanceIsZero = true;
break;
}
} else {
curPow += 8;
if (copyDistBits >= 0) {
break;
}
}
}
if (!distanceIsZero) {
// The distance value. Same shape as the length: two adaptive high bits, uniform in
// the middle, then three adaptive low bits whose +1/-1 adjustments keep the code
// ranges contiguous across bit counts.
if (copyDistBits < 0 || copyDistBits + 3 >= kCopyDistProbsSize) {
// Only reachable from a stream encoding a distance far larger than any real
// module needs - the hardware routine reads past its own table here.
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
u8 *probs = copyDistProbs + copyDistBits;
int readBits = copyDistBits / 8;
if (readBits < 3) {
copyDist = 1;
} else {
copyDist = 2 + dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
if (readBits > 3) {
copyDist = (copyDist << 1) | dec.ReadBit(probs + 3, kDecayNormal, kBonusNormal);
if (readBits > 4) {
copyDist = (copyDist << 1) | dec.ReadBitUniform();
readBits--;
}
while (readBits > 4) {
copyDist = (copyDist << 1) + dec.ReadBitUniformNoNorm();
readBits--;
}
}
}
copyDist <<= 1;
if (dec.ReadBit(probs, kDecayNormal, kBonusNormal)) {
if (readBits > 0) {
copyDist++;
}
} else if (readBits <= 0) {
copyDist--;
}
if (readBits > 0) {
copyDist <<= 1;
if (dec.ReadBit(probs + 1, kDecayNormal, kBonusNormal)) {
if (readBits != 1) {
copyDist++;
}
} else if (readBits == 1) {
copyDist--;
}
if (readBits != 1) {
copyDist <<= 1;
if (!dec.ReadBit(probs + 2, kDecayNormal, kBonusNormal)) {
copyDist--;
}
}
}
if (copyDist >= (u32)outPos) {
return SCE_KERNEL_ERROR_INVALID_FORMAT;
}
}
const int copyLen = (int)copyCount + 1;
// The hardware routine bounds-checks only the literal path, so a crafted stream can
// write up to 255 bytes past the end of the output buffer here. Check it.
if (copyLen > outSize - outPos) {
return SCE_KERNEL_ERROR_INVALID_SIZE;
}
// Forward, byte at a time - overlap is legal and is how runs are encoded.
const u8 *src = out + outPos - copyDist - 1;
for (int i = 0; i < copyLen; i++) {
out[outPos + i] = src[i];
}
prevByte = out[outPos + (int)copyCount];
outPos += (int)copyCount;
countBitsIdx = 6 + (outPos & 1);
}
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright (c) 2026- PPSSPP Project.
// 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, version 2.0 or later versions.
// 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include <cstddef>
#include "Common/CommonTypes.h"
// KL4E / KL3E - Sony's LZ77-with-arithmetic-coding scheme for compressed PSP executables, the
// alternative to gzip inside a ~PSP module. KL3E is the same bitstream with one constant changed.
//
// See docs/KL4E.md for the format.
// Decompresses one KL4E/KL3E stream. `in` points at the five-byte stream header, i.e. *past* the
// four-byte "KL4E"/"KL3E" magic. `inSize` bounds reads so a truncated file can't run off the end.
//
// Returns the number of bytes written, or a negative PSP error code:
// SCE_KERNEL_ERROR_INVALID_SIZE (0x80000104) output didn't fit
// SCE_KERNEL_ERROR_INVALID_FORMAT (0x80000108) stream is malformed
//
// If `end` is non-null it receives a pointer just past the last input byte consumed. (The PSP's
// own routine reports the last byte consumed for stored streams and one past for compressed ones;
// this is deliberately consistent instead, since nothing in PPSSPP depends on the quirk.)
int DecompressKL4E(u8 *out, int outSize, const u8 *in, size_t inSize, const u8 **end, bool isKL3E);
// True if the buffer starts with a "KL4E" or "KL3E" magic. Sets *isKL3E when it does.
bool IsKL4EMagic(const u8 *data, size_t size, bool *isKL3E);
+2
View File
@@ -336,6 +336,7 @@
<ClInclude Include="..\..\Core\Util\DisArm64.h" />
<ClInclude Include="..\..\Core\Util\GameManager.h" />
<ClInclude Include="..\..\Core\Util\PPGeDraw.h" />
<ClInclude Include="..\..\Core\Util\KL4E.h" />
<ClInclude Include="..\..\Core\Util\PSARUnpack.h" />
<ClInclude Include="..\..\Core\WaveFile.h" />
<ClInclude Include="..\..\ext\cityhash\city.h" />
@@ -655,6 +656,7 @@
<ClCompile Include="..\..\Core\Util\DisArm64.cpp" />
<ClCompile Include="..\..\Core\Util\GameManager.cpp" />
<ClCompile Include="..\..\Core\Util\PPGeDraw.cpp" />
<ClCompile Include="..\..\Core\Util\KL4E.cpp" />
<ClCompile Include="..\..\Core\Util\PSARUnpack.cpp" />
<ClCompile Include="..\..\Core\WaveFile.cpp" />
<ClCompile Include="..\..\ext\cityhash\city.cpp" />
+1
View File
@@ -279,6 +279,7 @@
<ClCompile Include="..\..\Core\Util\DisArm64.cpp" />
<ClCompile Include="..\..\Core\Util\GameManager.cpp" />
<ClCompile Include="..\..\Core\Util\PPGeDraw.cpp" />
<ClCompile Include="..\..\Core\Util\KL4E.cpp" />
<ClCompile Include="..\..\Core\Util\PSARUnpack.cpp" />
<ClCompile Include="..\..\Core\WaveFile.cpp" />
<ClCompile Include="..\..\ext\cityhash\city.cpp" />
+1
View File
@@ -811,6 +811,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Util/GameManager.cpp \
$(SRC)/Core/Util/BlockAllocator.cpp \
$(SRC)/Core/Util/PPGeDraw.cpp \
$(SRC)/Core/Util/KL4E.cpp \
$(SRC)/Core/Util/PSARUnpack.cpp \
$(SRC)/Core/Util/RecentFiles.cpp \
$(SRC)/Core/Util/VideoPlayer.cpp \
+1
View File
@@ -900,6 +900,7 @@ SOURCES_CXX += \
$(COREDIR)/Util/BlockAllocator.cpp \
$(COREDIR)/Util/MemStick.cpp \
$(COREDIR)/Util/PPGeDraw.cpp \
$(COREDIR)/Util/KL4E.cpp \
$(COREDIR)/Util/PSARUnpack.cpp \
$(COREDIR)/Util/RecentFiles.cpp \
$(COREDIR)/Util/AudioFormat.cpp \