Cache packfile index offsets
Cache packfile index offsets

<?php <?php
/** /**
* GitPHP Pack * GitPHP Pack
* *
* Extracts data from a pack * Extracts data from a pack
* Based on code from Glip by Patrik Fimml * Based on code from Glip by Patrik Fimml
* *
* @author Christopher Han <xiphux@gmail.com> * @author Christopher Han <xiphux@gmail.com>
* @copyright Copyright (c) 2011 Christopher Han * @copyright Copyright (c) 2011 Christopher Han
* @package GitPHP * @package GitPHP
* @subpackage Git * @subpackage Git
*/ */
   
/** /**
* Pack class * Pack class
* *
* @package GitPHP * @package GitPHP
* @subpackage Git * @subpackage Git
*/ */
class GitPHP_Pack class GitPHP_Pack
{ {
   
const OBJ_COMMIT = 1; const OBJ_COMMIT = 1;
const OBJ_TREE = 2; const OBJ_TREE = 2;
const OBJ_BLOB = 3; const OBJ_BLOB = 3;
const OBJ_TAG = 4; const OBJ_TAG = 4;
const OBJ_OFS_DELTA = 6; const OBJ_OFS_DELTA = 6;
const OBJ_REF_DELTA = 7; const OBJ_REF_DELTA = 7;
   
/** /**
* project * project
* *
* Stores the project internally * Stores the project internally
* *
* @access protected * @access protected
*/ */
protected $project; protected $project;
   
/** /**
* hash * hash
* *
* Stores the hash of the pack * Stores the hash of the pack
* *
* @access protected * @access protected
*/ */
protected $hash; protected $hash;
   
/** /**
  * offsetCache
  *
  * Caches object offsets
  *
  * @access protected
  */
  protected $offsetCache = array();
   
  /**
  * indexModified
  *
  * Stores the index file last modified time
  *
  * @access protected
  */
  protected $indexModified = 0;
   
  /**
* __construct * __construct
* *
* Instantiates object * Instantiates object
* *
* @access public * @access public
* @param mixed $project the project * @param mixed $project the project
* @param string $hash pack hash * @param string $hash pack hash
* @return mixed pack object * @return mixed pack object
* @throws Exception exception on invalid hash * @throws Exception exception on invalid hash
*/ */
public function __construct($project, $hash) public function __construct($project, $hash)
{ {
if (!(preg_match('/[0-9A-Fa-f]{40}/', $hash))) { if (!(preg_match('/[0-9A-Fa-f]{40}/', $hash))) {
throw new Exception(sprintf(__('Invalid hash %1$s'), $hash)); throw new Exception(sprintf(__('Invalid hash %1$s'), $hash));
} }
$this->hash = $hash; $this->hash = $hash;
$this->project = $project; $this->project = $project;
   
if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.idx')) { if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.idx')) {
throw new Exception('Pack index does not exist'); throw new Exception('Pack index does not exist');
} }
if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.pack')) { if (!file_exists($project->GetPath() . '/objects/pack/pack-' . $hash . '.pack')) {
throw new Exception('Pack file does not exist'); throw new Exception('Pack file does not exist');
} }
} }
   
/** /**
* GetHash * GetHash
* *
* Gets the hash * Gets the hash
* *
* @access public * @access public
* @return string object hash * @return string object hash
*/ */
public function GetHash() public function GetHash()
{ {
return $this->hash; return $this->hash;
} }
   
/** /**
* ContainsObject * ContainsObject
* *
* Checks if an object exists in the pack * Checks if an object exists in the pack
* *
* @access public * @access public
* @param string $hash object hash * @param string $hash object hash
* @return boolean true if object is in pack * @return boolean true if object is in pack
*/ */
public function ContainsObject($hash) public function ContainsObject($hash)
{ {
if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) { if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) {
return false; return false;
} }
   
return $this->FindPackedObject($hash) !== false; return $this->FindPackedObject($hash) !== false;
} }
   
/** /**
* FindPackedObject * FindPackedObject
* *
* Searches for an object's offset in the index * Searches for an object's offset in the index
* *
* @return int offset * @return int offset
* @param string $hash hash * @param string $hash hash
* @access private * @access private
*/ */
private function FindPackedObject($hash) private function FindPackedObject($hash)
{ {
if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) { if (!preg_match('/[0-9a-fA-F]{40}/', $hash)) {
return false; return false;
} }
   
  $indexFile = $this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.idx';
  $mTime = filemtime($indexFile);
  if ($mTime > $this->indexModified) {
  $this->offsetCache = array();
  $this->indexModified = $mTime;
  }
   
  if (isset($this->offsetCache[$hash])) {
  return $this->offsetCache[$hash];
  }
   
$offset = false; $offset = false;
   
$index = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.idx', 'rb'); $index = fopen($indexFile, 'rb');
flock($index, LOCK_SH); flock($index, LOCK_SH);
   
$magic = fread($index, 4); $magic = fread($index, 4);
if ($magic == "\xFFtOc") { if ($magic == "\xFFtOc") {
$version = GitPHP_Pack::fuint32($index); $version = GitPHP_Pack::fuint32($index);
if ($version == 2) { if ($version == 2) {
$offset = $this->SearchIndexV2($index, $hash); $offset = $this->SearchIndexV2($index, $hash);
} }
} else { } else {
$offset = $this->SearchIndexV1($index, $hash); $offset = $this->SearchIndexV1($index, $hash);
} }
flock($index, LOCK_UN); flock($index, LOCK_UN);
fclose($index); fclose($index);
  $this->offsetCache[$hash] = $offset;
return $offset; return $offset;
} }
   
/** /**
* SearchIndexV1 * SearchIndexV1
* *
* Seraches a version 1 index for a hash * Seraches a version 1 index for a hash
* *
* @access private * @access private
* @param resource $index file pointer to index * @param resource $index file pointer to index
* @param string $hash hash to find * @param string $hash hash to find
* @return int pack offset if found * @return int pack offset if found
*/ */
private function SearchIndexV1($index, $hash) private function SearchIndexV1($index, $hash)
{ {
/* /*
* index v1 struture: * index v1 struture:
* fanout table - 256*4 bytes * fanout table - 256*4 bytes
* offset/sha table - 24*count bytes (4 byte offset + 20 byte sha for each index) * offset/sha table - 24*count bytes (4 byte offset + 20 byte sha for each index)
*/ */
   
$binaryHash = pack('H40', $hash); $binaryHash = pack('H40', $hash);
   
/* /*
* get the start/end indices to search * get the start/end indices to search
* from the fanout table * from the fanout table
*/ */
list($low, $high) = $this->ReadFanout($index, $binaryHash, 0); list($low, $high) = $this->ReadFanout($index, $binaryHash, 0);
   
if ($low == $high) { if ($low == $high) {
return false; return false;
} }
   
/* /*
* binary serach for the index of the hash in the sha/offset listing * binary serach for the index of the hash in the sha/offset listing
* between cur and after from the fanout * between cur and after from the fanout
*/ */
while ($low <= $high) { while ($low <= $high) {
$mid = ($low + $high) >> 1; $mid = ($low + $high) >> 1;
fseek($index, 4*256 + 24*$mid); fseek($index, 4*256 + 24*$mid);
   
$off = GitPHP_Pack::fuint32($index); $off = GitPHP_Pack::fuint32($index);
$binName = fread($index, 20); $binName = fread($index, 20);
$name = bin2hex($binName); $name = bin2hex($binName);
   
  $this->offsetCache[$name] = $off;
   
$cmp = strcmp($hash, $name); $cmp = strcmp($hash, $name);
if ($cmp < 0) { if ($cmp < 0) {
$high = $mid - 1; $high = $mid - 1;
} else if ($cmp > 0) { } else if ($cmp > 0) {
$low = $mid + 1; $low = $mid + 1;
} else { } else {
return $off; return $off;
} }
} }
   
return false; return false;
} }
   
/** /**
* SearchIndexV2 * SearchIndexV2
* *
* Seraches a version 2 index for a hash * Seraches a version 2 index for a hash
* *
* @access private * @access private
* @param resource $index file pointer to index * @param resource $index file pointer to index
* @param string $hash hash to find * @param string $hash hash to find
* @return int pack offset if found * @return int pack offset if found
*/ */
private function SearchIndexV2($index, $hash) private function SearchIndexV2($index, $hash)
{ {
/* /*
* index v2 structure: * index v2 structure:
* magic and version - 2*4 bytes * magic and version - 2*4 bytes
* fanout table - 256*4 bytes * fanout table - 256*4 bytes
* sha listing - 20*count bytes * sha listing - 20*count bytes
* crc checksums - 4*count bytes * crc checksums - 4*count bytes
* offsets - 4*count bytes * offsets - 4*count bytes
*/ */
$binaryHash = pack('H40', $hash); $binaryHash = pack('H40', $hash);
   
/* /*
* get the start/end indices to search * get the start/end indices to search
* from the fanout table * from the fanout table
*/ */
list($low, $high) = $this->ReadFanout($index, $binaryHash, 8); list($low, $high) = $this->ReadFanout($index, $binaryHash, 8);
if ($low == $high) { if ($low == $high) {
return false; return false;
} }
   
/* /*
* get the object count from fanout[255] * get the object count from fanout[255]
*/ */
fseek($index, 8 + 4*255); fseek($index, 8 + 4*255);
$objectCount = GitPHP_Pack::fuint32($index); $objectCount = GitPHP_Pack::fuint32($index);
   
/* /*
* binary search for the index of the hash in the sha listing * binary search for the index of the hash in the sha listing
* between cur and after from the fanout * between cur and after from the fanout
*/ */
$objIndex = false; $objIndex = false;
while ($low <= $high) { while ($low <= $high) {
$mid = ($low + $high) >> 1; $mid = ($low + $high) >> 1;
fseek($index, 8 + 4*256 + 20*$mid); fseek($index, 8 + 4*256 + 20*$mid);
   
$binName = fread($index, 20); $binName = fread($index, 20);
$name = bin2hex($binName); $name = bin2hex($binName);
   
$cmp = strcmp($hash, $name); $cmp = strcmp($hash, $name);
   
if ($cmp < 0) { if ($cmp < 0) {
$high = $mid - 1; $high = $mid - 1;
} else if ($cmp > 0) { } else if ($cmp > 0) {
$low = $mid + 1; $low = $mid + 1;
} else { } else {
$objIndex = $mid; $objIndex = $mid;
break; break;
} }
} }
if ($objIndex === false) { if ($objIndex === false) {
return false; return false;
} }
   
/* /*
* get the offset from the same index in the offset table * get the offset from the same index in the offset table
*/ */
fseek($index, 8 + 4*256 + 24*$objectCount + 4*$objIndex); fseek($index, 8 + 4*256 + 24*$objectCount + 4*$objIndex);
$offset = GitPHP_Pack::fuint32($index); $offset = GitPHP_Pack::fuint32($index);
if ($offset & 0x80000000) { if ($offset & 0x80000000) {
throw new Exception('64-bit offsets not implemented'); throw new Exception('64-bit offsets not implemented');
} }
return $offset; return $offset;
} }
   
/** /**
* ReadFanout * ReadFanout
* *
* Finds the start/end index a hash will be located between, * Finds the start/end index a hash will be located between,
* acconding to the fanout table * acconding to the fanout table
* *
* @access private * @access private
* @param resource $index index file pointer * @param resource $index index file pointer
* @param string $binaryHash binary encoded hash to find * @param string $binaryHash binary encoded hash to find
* @param int $offset offset in the index file where the fanout table is located * @param int $offset offset in the index file where the fanout table is located
* @return array Range where object can be located * @return array Range where object can be located
*/ */
private function ReadFanout($index, $binaryHash, $offset) private function ReadFanout($index, $binaryHash, $offset)
{ {
/* /*
* fanout table has 255 4-byte integers * fanout table has 255 4-byte integers
* indexed by the first byte of the object name. * indexed by the first byte of the object name.
* the value at that index is the index at which objects * the value at that index is the index at which objects
* starting with that byte can be found * starting with that byte can be found
* (first level fan-out) * (first level fan-out)
*/ */
if ($binaryHash{0} == "\x00") { if ($binaryHash{0} == "\x00") {
$low = 0; $low = 0;
fseek($index, $offset); fseek($index, $offset);
$high = GitPHP_Pack::fuint32($index); $high = GitPHP_Pack::fuint32($index);
} else { } else {
fseek($index, $offset + (ord($binaryHash{0}) - 1) * 4); fseek($index, $offset + (ord($binaryHash{0}) - 1) * 4);
$low = GitPHP_Pack::fuint32($index); $low = GitPHP_Pack::fuint32($index);
$high = GitPHP_Pack::fuint32($index); $high = GitPHP_Pack::fuint32($index);
} }
return array($low, $high); return array($low, $high);
} }
   
/** /**
* GetObject * GetObject
* *
* Extracts an object from the pack * Extracts an object from the pack
* *
* @access public * @access public
* @param string $hash hash of object to extract * @param string $hash hash of object to extract
* @param int $type output parameter, returns the type of the object * @param int $type output parameter, returns the type of the object
* @return string object content, or false if not found * @return string object content, or false if not found
*/ */
public function GetObject($hash, &$type = 0) public function GetObject($hash, &$type = 0)
{ {
$offset = $this->FindPackedObject($hash); $offset = $this->FindPackedObject($hash);
if ($offset === false) { if ($offset === false) {
return false; return false;
} }
   
$pack = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.pack', 'rb'); $pack = fopen($this->project->GetPath() . '/objects/pack/pack-' . $this->hash . '.pack', 'rb');
flock($pack, LOCK_SH); flock($pack, LOCK_SH);
   
$magic = fread($pack, 4); $magic = fread($pack, 4);
$version = GitPHP_Pack::fuint32($pack); $version = GitPHP_Pack::fuint32($pack);
if ($magic != 'PACK' || $version != 2) { if ($magic != 'PACK' || $version != 2) {
flock($pack, LOCK_UN); flock($pack, LOCK_UN);
fclose($pack); fclose($pack);
throw new Exception('Unsupported pack format'); throw new Exception('Unsupported pack format');
} }
   
list($type, $data) = $this->UnpackObject($pack, $offset); list($type, $data) = $this->UnpackObject($pack, $offset);
   
flock($pack, LOCK_UN); flock($pack, LOCK_UN);
fclose($pack); fclose($pack);
return $data; return $data;
} }
   
/** /**
* UnpackObject * UnpackObject
* *
* Extracts an object at an offset * Extracts an object at an offset
* *
* @access private * @access private
* @param resource $pack pack file pointer * @param resource $pack pack file pointer
* @param int $offset object offset * @param int $offset object offset
* @return array object type and data * @return array object type and data
*/ */
private function UnpackObject($pack, $offset) private function UnpackObject($pack, $offset)
{ {
fseek($pack, $offset); fseek($pack, $offset);
   
/* /*
* object header: * object header:
* first byte is the type (high 3 bits) and low byte of size (lower 4 bits) * first byte is the type (high 3 bits) and low byte of size (lower 4 bits)
* subsequent bytes each have 7 next higher bits of the size (little endian) * subsequent bytes each have 7 next higher bits of the size (little endian)
* most significant bit is either 1 or 0 to indicate whether the next byte * most significant bit is either 1 or 0 to indicate whether the next byte
* should be read as part of the size. 1 means continue reading the size, * should be read as part of the size. 1 means continue reading the size,
* 0 means the data is starting * 0 means the data is starting
*/ */
$c = ord(fgetc($pack)); $c = ord(fgetc($pack));
$type = ($c >> 4) & 0x07; $type = ($c >> 4) & 0x07;
$size = $c & 0x0F; $size = $c & 0x0F;
for ($i = 4; $c & 0x80; $i += 7) { for ($i = 4; $c & 0x80; $i += 7) {
$c = ord(fgetc($pack)); $c = ord(fgetc($pack));
$size |= (($c & 0x7f) << $i); $size |= (($c & 0x7f) << $i);
} }
   
if ($type == GitPHP_Pack::OBJ_COMMIT || $type == GitPHP_Pack::OBJ_TREE || $type == GitPHP_Pack::OBJ_BLOB || $type == GitPHP_Pack::OBJ_TAG) { if ($type == GitPHP_Pack::OBJ_COMMIT || $type == GitPHP_Pack::OBJ_TREE || $type == GitPHP_Pack::OBJ_BLOB || $type == GitPHP_Pack::OBJ_TAG) {
/* /*
* regular gzipped object data * regular gzipped object data
*/ */
return array($type, gzuncompress(fread($pack, $size+512), $size)); return array($type, gzuncompress(fread($pack, $size+512), $size));
} else if ($type == GitPHP_Pack::OBJ_OFS_DELTA) { } else if ($type == GitPHP_Pack::OBJ_OFS_DELTA) {
/* /*
* delta of an object at offset * delta of an object at offset
*/ */
$buf = fread($pack, $size+512+20); $buf = fread($pack, $size+512+20);
   
/* /*
* read the base object offset * read the base object offset
* each subsequent byte's 7 least significant bits * each subsequent byte's 7 least significant bits
* are part of the offset in decreasing significance per byte * are part of the offset in decreasing significance per byte
* (opposite of other places) * (opposite of other places)
* most significant bit is a flag indicating whether to read the * most significant bit is a flag indicating whether to read the
* next byte as part of the offset * next byte as part of the offset
*/ */
$pos = 0; $pos = 0;
$off = -1; $off = -1;
do { do {
$off++; $off++;
$c = ord($buf{$pos++}); $c = ord($buf{$pos++});
$off = ($off << 7) + ($c & 0x7f); $off = ($off << 7) + ($c & 0x7f);
} while ($c & 0x80); } while ($c & 0x80);
   
/* /*
* next read the compressed delta data * next read the compressed delta data
*/ */
$delta = gzuncompress(substr($buf, $pos), $size); $delta = gzuncompress(substr($buf, $pos), $size);
unset($buf); unset($buf);
   
$baseOffset = $offset - $off; $baseOffset = $offset - $off;
if ($baseOffset > 0) { if ($baseOffset > 0) {
/* /*
* read base object at offset and apply delta to it * read base object at offset and apply delta to it
*/ */
list($type, $base) = $this->UnpackObject($pack, $baseOffset); list($type, $base) = $this->UnpackObject($pack, $baseOffset);
$data = GitPHP_Pack::ApplyDelta($delta, $base); $data = GitPHP_Pack::ApplyDelta($delta, $base);
return array($type, $data); return array($type, $data);
} }
} else if ($type == GitPHP_Pack::OBJ_REF_DELTA) { } else if ($type == GitPHP_Pack::OBJ_REF_DELTA) {
/* /*
* delta of object with hash * delta of object with hash
*/ */
   
/* /*
* first the base object's hash * first the base object's hash
* load that object * load that object
*/ */
$hash = fread($pack, 20); $hash = fread($pack, 20);
$hash = bin2hex($hash); $hash = bin2hex($hash);
$base = $this->project->GetObject($hash, $type); $base = $this->project->GetObject($hash, $type);
   
/* /*
* then the gzipped delta data * then the gzipped delta data
*/ */
$delta = gzuncompress(fread($pack, $size + 512), $size); $delta = gzuncompress(fread($pack, $size + 512), $size);
   
$data = GitPHP_Pack::ApplyDelta($delta, $base); $data = GitPHP_Pack::ApplyDelta($delta, $base);
   
return array($type, $data); return array($type, $data);
} }
   
return false; return false;
} }
   
/** /**
* ApplyDelta * ApplyDelta
* *
* Applies a binary delta to a base object * Applies a binary delta to a base object
* *
* @static * @static
* @access private * @access private
* @param string $delta delta string * @param string $delta delta string
* @param string $base base object data * @param string $base base object data
* @return string patched content * @return string patched content
*/ */
private static function ApplyDelta($delta, $base) private static function ApplyDelta($delta, $base)
{ {
/* /*
* algorithm from patch-delta.c * algorithm from patch-delta.c
*/ */
$pos = 0; $pos = 0;
$baseSize = GitPHP_Pack::ParseVarInt($delta, $pos); $baseSize = GitPHP_Pack::ParseVarInt($delta, $pos);
$resultSize = GitPHP_Pack::ParseVarInt($delta, $pos); $resultSize = GitPHP_Pack::ParseVarInt($delta, $pos);
   
$data = ''; $data = '';
$deltalen = strlen($delta); $deltalen = strlen($delta);
while ($pos < $deltalen) { while ($pos < $deltalen) {
$opcode = ord($delta{$pos++}); $opcode = ord($delta{$pos++});
if ($opcode & 0x80) { if ($opcode & 0x80) {
$off = 0; $off = 0;
if ($opcode & 0x01) $off = ord($delta{$pos++}); if ($opcode & 0x01) $off = ord($delta{$pos++});
if ($opcode & 0x02) $off |= ord($delta{$pos++}) << 8; if ($opcode & 0x02) $off |= ord($delta{$pos++}) << 8;
if ($opcode & 0x04) $off |= ord($delta{$pos++}) << 16; if ($opcode & 0x04) $off |= ord($delta{$pos++}) << 16;
if ($opcode & 0x08) $off |= ord($delta{$pos++}) << 24; if ($opcode & 0x08) $off |= ord($delta{$pos++}) << 24;
$len = 0; $len = 0;
if ($opcode & 0x10) $len = ord($delta{$pos++}); if ($opcode & 0x10) $len = ord($delta{$pos++});
if ($opcode & 0x20) $len |= ord($delta{$pos++}) << 8; if ($opcode & 0x20) $len |= ord($delta{$pos++}) << 8;
if ($opcode & 0x40) $len |= ord($delta{$pos++}) << 16; if ($opcode & 0x40) $len |= ord($delta{$pos++}) << 16;
if ($len == 0) $len = 0x10000; if ($len == 0) $len = 0x10000;
$data .= substr($base, $off, $len); $data .= substr($base, $off, $len);
} else if ($opcode > 0) { } else if ($opcode > 0) {
$data .= substr($delta, $pos, $opcode); $data .= substr($delta, $pos, $opcode);
$pos += $opcode; $pos += $opcode;
} }
} }
return $data; return $data;
} }
   
/** /**
* ParseVarInt * ParseVarInt
* *
* Reads a git-style packed variable length integer * Reads a git-style packed variable length integer
* sequence of bytes, where each byte's 7 less significant bits * sequence of bytes, where each byte's 7 less significant bits
* are pieces of the int in increasing significance for each byte (little endian) * are pieces of the int in increasing significance for each byte (little endian)
* the most significant bit of each byte is a flag whether to continue * the most significant bit of each byte is a flag whether to continue
* reading bytes or not * reading bytes or not
* *
* @access private * @access private
* @static * @static
* @param string $str packed data string * @param string $str packed data string
* @param int $pos position in string to read from * @param int $pos position in string to read from
* @return int parsed integer * @return int parsed integer
*/ */
private static function ParseVarInt($str, &$pos=0) private static function ParseVarInt($str, &$pos=0)
{ {
$ret = 0; $ret = 0;
$byte = 0x80; $byte = 0x80;
for ($shift = 0; $byte & 0x80; $shift += 7) { for ($shift = 0; $byte & 0x80; $shift += 7) {
$byte = ord($str{$pos++}); $byte = ord($str{$pos++});
$ret |= (($byte & 0x7F) << $shift); $ret |= (($byte & 0x7F) << $shift);
} }
return $ret; return $ret;
} }
   
/** /**
* uint32 * uint32
* *
* Unpacks a packed 32 bit integer * Unpacks a packed 32 bit integer
* *
* @static * @static
* @access private * @access private
* @return int integer * @return int integer
* @param string $str binary data * @param string $str binary data
*/ */
private static function uint32($str) private static function uint32($str)
{ {
$a = unpack('Nx', substr($str, 0, 4)); $a = unpack('Nx', substr($str, 0, 4));
return $a['x']; return $a['x'];
} }
   
/** /**
* fuint32 * fuint32
* *
* Reads and unpacks the next 32 bit integer * Reads and unpacks the next 32 bit integer
* *
* @static * @static
* @access private * @access private
* @return int integer * @return int integer
* @param resource $handle file handle * @param resource $handle file handle
*/ */
private static function fuint32($handle) private static function fuint32($handle)
{ {
return GitPHP_Pack::uint32(fread($handle, 4)); return GitPHP_Pack::uint32(fread($handle, 4));
} }
} }
   
comments