api#326 Сохранить прежний random и чужие JSON токены
deploy-preland / deploy zoomstage.online (push) Successful in 45s

This commit is contained in:
Panfilov Konstantin committed 2026-10-25 02:10:45 +03:00
1 parent 81e189ca67
commit 08febba368
4 files changed
+124 -35

No files matched your search

+85 -25
View File
@@ -1,34 +1,94 @@
<?php <?php
/** api#326: только согласованные имена/enum UTM; чужие поля/JSON-типы сохраняются. */ /** api#326: заменяет только согласованные JSON-токены, сохраняя прочие байты. */
declare(strict_types=1); declare(strict_types=1);
/** Карта диапазонов исходного JSON; числовые токены никогда не сериализуются. */
function utmConfigNode(string $raw, int &$offset): array
{
$length=strlen($raw);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$start=$offset; $char=$raw[$offset] ?? '';
if ($char==='"') {
$offset++;
while ($offset<$length) {
if ($raw[$offset]==='\\') { $offset+=2; continue; }
if ($raw[$offset++]==='"') break;
}
return ['start'=>$start,'end'=>$offset,'type'=>'string','value'=>json_decode(substr($raw,$start,$offset-$start),true,512,JSON_THROW_ON_ERROR)];
}
if ($char==='{' || $char==='[') {
$object=$char==='{'; $close=$object ? '}' : ']'; $offset++; $properties=[];
while (true) {
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===$close) { $offset++; break; }
if ($object) {
$key=utmConfigNode($raw,$offset);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$offset++; // Двоеточие уже проверено полным JSON validation.
$value=utmConfigNode($raw,$offset);
$properties[]=['key'=>$key,'value'=>$value];
} else { utmConfigNode($raw,$offset); }
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===',') { $offset++; continue; }
$offset++; break;
}
return ['start'=>$start,'end'=>$offset,'type'=>$object ? 'object' : 'array','properties'=>$properties];
}
while ($offset<$length && !str_contains(",]} \t\r\n",$raw[$offset])) $offset++;
return ['start'=>$start,'end'=>$offset,'type'=>'scalar'];
}
/** Выбирает единственное свойство; неоднозначные управляющие поля не мигрирует. */
function utmConfigProperty(array $node, string $key): ?array
{
$found=null;
foreach ($node['properties'] ?? [] as $property) {
if ($property['key']['value']!==$key) continue;
if ($found!==null) throw new RuntimeException('Повторное управляющее поле: '.$key);
$found=$property;
}
return $found;
}
if ($argc!==2) { fwrite(STDERR,"Использование: php bin/ensure-utm-config.php config.json\n"); exit(2); } if ($argc!==2) { fwrite(STDERR,"Использование: php bin/ensure-utm-config.php config.json\n"); exit(2); }
try { try {
$path=$argv[1]; $raw=file_get_contents($path); $path=$argv[1]; $raw=file_get_contents($path);
$config=json_decode($raw,false,512,JSON_THROW_ON_ERROR); // Валидация всей грамматики без последующей сериализации decoded данных.
if (!$config instanceof stdClass || !($config->utm ?? null) instanceof stdClass) throw new RuntimeException('Отсутствует объект utm'); json_decode($raw,false,512,JSON_THROW_ON_ERROR|JSON_BIGINT_AS_STRING);
$before=serialize($config); $offset=0; $root=utmConfigNode($raw,$offset);
foreach (['registration'=>'reg','bchoice'=>'chob'] as $old=>$new) { $utm=utmConfigProperty($root,'utm')['value'] ?? null;
if (property_exists($config->utm,$old)) { if (($root['type'] ?? null)!=='object' || ($utm['type'] ?? null)!=='object') throw new RuntimeException('Отсутствует объект utm');
if (property_exists($config->utm,$new)) throw new RuntimeException('Конфликт старого и нового UTM: '.$new); $replacements=[];
$config->utm->$new=$config->utm->$old; unset($config->utm->$old); $replace=static function(array $node,string $value) use (&$replacements): void {
} $replacements[]=['start'=>$node['start'],'end'=>$node['end'],'text'=>json_encode($value,JSON_THROW_ON_ERROR)];
} };
foreach (['theme'=>['dark'=>'1','light'=>'2'],'reg'=>['before'=>'1','after'=>'2'],'chob'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) { foreach (['theme'=>['dark'=>'1','light'=>'2'],'reg'=>['before'=>'1','after'=>'2'],'chob'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) {
if (!property_exists($config->utm,$key)) continue; $old=['reg'=>'registration','chob'=>'bchoice'][$key] ?? null;
$setting=$config->utm->$key; $property=utmConfigProperty($utm,$key);
if (!$setting instanceof stdClass || !($setting->weights ?? null) instanceof stdClass || !(is_string($setting->default ?? null) || is_int($setting->default ?? null))) throw new RuntimeException('Некорректный UTM: '.$key); $legacy=$old===null ? null : utmConfigProperty($utm,$old);
$weights=new stdClass; if ($property && $legacy) throw new RuntimeException('Конфликт старого и нового UTM: '.$key);
foreach ($setting->weights as $variant=>$weight) { if ($legacy) { $replace($legacy['key'],$key); $property=$legacy; }
$digit=$map[$variant] ?? (string)$variant; if (!$property) continue;
if (!in_array($digit,['1','2'],true) || property_exists($weights,$digit)) throw new RuntimeException('Неоднозначный вариант UTM: '.$key); $setting=$property['value'];
$weights->$digit=$weight; $weights=utmConfigProperty($setting,'weights')['value'] ?? null;
$default=utmConfigProperty($setting,'default')['value'] ?? null;
if (($setting['type'] ?? null)!=='object' || ($weights['type'] ?? null)!=='object' || !$default) throw new RuntimeException('Некорректный UTM: '.$key);
$seen=[];
foreach ($weights['properties'] as $variant) {
$name=$variant['key']['value']; $digit=$map[$name] ?? $name;
if (!in_array($digit,['1','2'],true) || isset($seen[$digit])) throw new RuntimeException('Неоднозначный вариант UTM: '.$key);
$seen[$digit]=true;
if ($digit!==$name) $replace($variant['key'],$digit);
} }
$default=$map[$setting->default] ?? $setting->default; $value=json_decode(substr($raw,$default['start'],$default['end']-$default['start']),false,512,JSON_THROW_ON_ERROR);
if (!in_array((string)$default,['1','2'],true)) throw new RuntimeException('Некорректный default UTM: '.$key); if (!(is_string($value)||is_int($value))) throw new RuntimeException('Некорректный default UTM: '.$key);
$setting->weights=$weights; $setting->default=$default; $digit=$map[$value] ?? $value;
if (!in_array((string)$digit,['1','2'],true)) throw new RuntimeException('Некорректный default UTM: '.$key);
if ($digit!==$value) $replace($default,$digit);
} }
if (serialize($config)===$before) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); } if (!$replacements) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); }
$encoded=json_encode($config,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n"; usort($replacements,static fn(array $a,array $b): int=>$b['start']<=>$a['start']);
if (file_put_contents($path,$encoded,LOCK_EX)===false) throw new RuntimeException('Не удалось записать config'); foreach ($replacements as $replacement) $raw=substr_replace($raw,$replacement['text'],$replacement['start'],$replacement['end']-$replacement['start']);
echo "Мигрированы только согласованные имена и enum UTM.\n"; exit(10); if (file_put_contents($path,$raw,LOCK_EX)===false) throw new RuntimeException('Не удалось записать config');
echo "Мигрированы только согласованные имена и enum UTM; остальные байты сохранены.\n"; exit(10);
} catch(Throwable $error) { fwrite(STDERR,$error->getMessage()."\n"); exit(2); } } catch(Throwable $error) { fwrite(STDERR,$error->getMessage()."\n"); exit(2); }
+26 -10
View File
@@ -50,16 +50,32 @@ function prelandResolveUtm(array $query, array $config): array
$value=$query[$key] ?? null; $value=$query[$key] ?? null;
$digit=is_string($value) && preg_match('/\A.{4}([0-9])/us',$value,$match) ? $match[1] : null; $digit=is_string($value) && preg_match('/\A.{4}([0-9])/us',$value,$match) ? $match[1] : null;
if ($digit==='0') { if ($digit==='0') {
$weights=[]; // Generic resolver исторически выбирает первый ключ при sum<=0;
foreach ((array)($settings['weights'] ?? []) as $variant=>$weight) { // cons имеет отдельный контракт: только положительные веса и default.
if ($valid($variant) && is_numeric($weight) && is_finite((float)$weight) && (float)$weight>0) $weights[$variant]=(float)$weight; if ($key !== 'cons') {
} $weights=(array)($settings['weights'] ?? []);
$total=array_sum($weights); $digit=$default; $total=array_sum($weights);
if ($total>0 && is_finite($total)) { $digit=$weights ? (string)array_key_first($weights) : $default;
$point=mt_rand()/(mt_getrandmax()+1.0)*$total; if ($total>0 && is_finite((float)$total)) {
foreach ($weights as $variant=>$weight) { $point=mt_rand()/mt_getrandmax()*$total; $current=0.0;
if ($point<$weight) { $digit=(string)$variant; break; } foreach ($weights as $variant=>$weight) {
$point-=$weight; $current+=(float)$weight;
$digit=(string)$variant;
if ($point<=$current) break;
}
}
} else {
$weights=[];
foreach ((array)($settings['weights'] ?? []) as $variant=>$weight) {
if ($valid($variant) && is_numeric($weight) && is_finite((float)$weight) && (float)$weight>0) $weights[$variant]=(float)$weight;
}
$total=array_sum($weights); $digit=$default;
if ($total>0 && is_finite($total)) {
$point=mt_rand()/(mt_getrandmax()+1.0)*$total;
foreach ($weights as $variant=>$weight) {
if ($point<$weight) { $digit=(string)$variant; break; }
$point-=$weight;
}
} }
} }
} }
+10
View File
@@ -21,5 +21,15 @@ try{
file_put_contents($file,'{"utm":{"registration":{"weights":{"before":1},"default":"before"},"reg":{"weights":{"1":1},"default":"1"}}}'); file_put_contents($file,'{"utm":{"registration":{"weights":{"before":1},"default":"before"},"reg":{"weights":{"1":1},"default":"1"}}}');
$conflict=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code); $conflict=file_get_contents($file);exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file).' 2>/dev/null',$out,$code);
if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('Неоднозначный config должен блокировать patch без записи'); if($code!==2||file_get_contents($file)!==$conflict)throw new RuntimeException('Неоднозначный config должен блокировать patch без записи');
$untouched='"foreign" : {"customFloat":1.0,"big":9223372036854775809,"nested":[1.00,{},[],{"exponent":2e+03,"negative":-0.0}]}';
$tokenFixture='{'.$untouched.',"utm":{"theme":{"weights":{"dark":1.0,"light":0},"default":"dark"},"custom":{"big":9223372036854775809}}}';
file_put_contents($file,$tokenFixture);
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
$patched=file_get_contents($file);
if($code!==10||!str_contains($patched,$untouched)||!str_contains($patched,'"custom":{"big":9223372036854775809}')||!str_contains($patched,'"weights":{"1":1.0,"2":0}'))throw new RuntimeException('Patch изменил исходные foreign/weight numeric tokens');
$expectedTokens=str_replace(['"dark":','"light":','"default":"dark"'],['"1":','"2":','"default":"1"'],$tokenFixture);
if($patched!==$expectedTokens)throw new RuntimeException('Изменены байты вне согласованных source ranges');
exec(PHP_BINARY.' '.escapeshellarg($script).' '.escapeshellarg($file),$out,$code);
if($code!==0||file_get_contents($file)!==$patched)throw new RuntimeException('Token patch не идемпотентен');
echo "OK: numeric config migration preserves weights/default/foreign JSON types\n"; echo "OK: numeric config migration preserves weights/default/foreign JSON types\n";
}finally{unlink($file);} }finally{unlink($file);}
+3
View File
@@ -25,4 +25,7 @@ foreach(array_keys($defaults) as $key) foreach(['1','rnd','abcdx','abc',[],null,
} }
if(prelandResolveUtm(['tp'=>'abcd9'],$settings)['digits']['tp']!=='2')throw new RuntimeException('Ненастроенная группа tp должна fallback'); if(prelandResolveUtm(['tp'=>'abcd9'],$settings)['digits']['tp']!=='2')throw new RuntimeException('Ненастроенная группа tp должна fallback');
if(prelandResolveUtm(['cons'=>'abcd9'],$settings)['digits']['cons']!=='9')throw new RuntimeException('Cons concrete не зависит от random weights'); if(prelandResolveUtm(['cons'=>'abcd9'],$settings)['digits']['cons']!=='9')throw new RuntimeException('Cons concrete не зависит от random weights');
$zero=['utm'=>['theme'=>['weights'=>['2'=>0,'1'=>0],'default'=>'1'],'cons'=>['weights'=>['1'=>0,'2'=>0],'default'=>'2']]];
if(prelandResolveUtm(['theme'=>'abcd0'],$zero)['digits']['theme']!=='2')throw new RuntimeException('Generic zero weights обязаны выбрать первый ключ, как до #326');
if(prelandResolveUtm(['cons'=>'abcd0'],$zero)['digits']['cons']!=='2')throw new RuntimeException('Cons zero weights обязаны сохранить special default');
echo "OK: numeric public boundary, defaults, weighted0, foreign values\n"; echo "OK: numeric public boundary, defaults, weighted0, foreign values\n";