@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DiffusionMercurialWireProtocol extends Phobject {
4
5 public static function getCommandArgs($command) {
6 // We need to enumerate all of the Mercurial wire commands because the
7 // argument encoding varies based on the command. "Why?", you might ask,
8 // "Why would you do this?".
9
10 $commands = array(
11 'batch' => array('cmds', '*'),
12 'between' => array('pairs'),
13 'branchmap' => array(),
14 'branches' => array('nodes'),
15 'capabilities' => array(),
16 'changegroup' => array('roots'),
17 'changegroupsubset' => array('bases heads'),
18 'debugwireargs' => array('one two *'),
19 'getbundle' => array('*'),
20 'heads' => array(),
21 'hello' => array(),
22 'known' => array('nodes', '*'),
23 'listkeys' => array('namespace'),
24 'lookup' => array('key'),
25 'pushkey' => array('namespace', 'key', 'old', 'new'),
26 'protocaps' => array('caps'),
27 'stream_out' => array(''),
28 'unbundle' => array('heads'),
29 );
30
31 if (!isset($commands[$command])) {
32 throw new Exception(
33 pht(
34 'Unknown Mercurial command "%s"!',
35 $command));
36 }
37
38 return $commands[$command];
39 }
40
41 public static function isReadOnlyCommand($command) {
42 $read_only = array(
43 'between' => true,
44 'branchmap' => true,
45 'branches' => true,
46 'capabilities' => true,
47 'changegroup' => true,
48 'changegroupsubset' => true,
49 'debugwireargs' => true,
50 'getbundle' => true,
51 'heads' => true,
52 'hello' => true,
53 'known' => true,
54 'listkeys' => true,
55 'lookup' => true,
56 'protocaps' => true,
57 'stream_out' => true,
58 );
59
60 // Notably, the write commands are "pushkey" and "unbundle". The
61 // "batch" command is theoretically read only, but we require explicit
62 // analysis of the actual commands.
63
64 return isset($read_only[$command]);
65 }
66
67 public static function isReadOnlyBatchCommand($cmds) {
68 if (!strlen($cmds)) {
69 // We expect a "batch" command to always have a "cmds" string, so err
70 // on the side of caution and throw if we don't get any data here. This
71 // either indicates a mangled command from the client or a programming
72 // error in our code.
73 throw new Exception(pht("Expected nonempty '%s' specification!", 'cmds'));
74 }
75
76 // For "batch" we get a "cmds" argument like:
77 //
78 // heads ;known nodes=
79 //
80 // We need to examine the commands (here, "heads" and "known") to make sure
81 // they're all read-only.
82
83 // NOTE: Mercurial has some code to escape semicolons, but it does not
84 // actually function for command separation. For example, these two batch
85 // commands will produce completely different results (the former will run
86 // the lookup; the latter will fail with a parser error):
87 //
88 // lookup key=a:xb;lookup key=z* 0
89 // lookup key=a:;b;lookup key=z* 0
90 // ^
91 // |
92 // +-- Note semicolon.
93 //
94 // So just split unconditionally.
95
96 $cmds = explode(';', $cmds);
97 foreach ($cmds as $sub_cmd) {
98 $name = head(explode(' ', $sub_cmd, 2));
99 if (!self::isReadOnlyCommand($name)) {
100 return false;
101 }
102 }
103
104 return true;
105 }
106
107 /** If the server version is running 3.4+ it will respond
108 * with 'bundle2' capability in the format of "bundle2=(url-encoding)".
109 * Until we manage to properly package up bundles to send back we
110 * disallow the client from knowing we speak bundle2 by removing it
111 * from the capabilities listing.
112 *
113 * The format of the capabilities string is: "a space separated list
114 * of strings representing what commands the server supports"
115 * @link https://www.mercurial-scm.org/wiki/CommandServer#Protocol
116 *
117 * @param string $capabilities - The string of capabilities to
118 * strip the bundle2 capability from. This is expected to be
119 * the space-separated list of strings resulting from the
120 * querying the 'capabilities' command.
121 *
122 * @return string The resulting space-separated list of capabilities
123 * which no longer contains the 'bundle2' capability. This is meant
124 * to replace the original $body to send back to client.
125 */
126 public static function filterBundle2Capability($capabilities) {
127 $parts = explode(' ', $capabilities);
128 foreach ($parts as $key => $part) {
129 if (preg_match('/^bundle2=/', $part)) {
130 unset($parts[$key]);
131 break;
132 }
133 }
134 return implode(' ', $parts);
135 }
136
137}