$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); } try { $path=$argv[1]; $raw=file_get_contents($path); // Валидация всей грамматики без последующей сериализации decoded данных. json_decode($raw,false,512,JSON_THROW_ON_ERROR|JSON_BIGINT_AS_STRING); $offset=0; $root=utmConfigNode($raw,$offset); // Проверяем source keys, а не decoded object: escaped/повторные имена // тоже не должны скрыть chb перед публикацией enum replacement. foreach($root['properties']??[] as $property){ if($property['key']['value']!=='utm'||($property['value']['type']??null)!=='object')continue; foreach($property['value']['properties'] as $setting)if($setting['key']['value']==='chb')throw new RuntimeException('Устаревший ключ utm.chb нельзя публиковать. Явно удалите его перед миграцией.'); } $utm=utmConfigProperty($root,'utm')['value'] ?? null; if (($root['type'] ?? null)!=='object' || ($utm['type'] ?? null)!=='object') throw new RuntimeException('Отсутствует объект utm'); $replacements=[]; $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'],'banch'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) { $property=utmConfigProperty($utm,$key); $aliases=['reg'=>['registration'],'banch'=>['chob','bchoice']][$key] ?? []; foreach ($aliases as $old) { $legacy=utmConfigProperty($utm,$old); if ($property && $legacy) throw new RuntimeException('Конфликт старого и нового UTM: '.$key); if ($legacy) { $replace($legacy['key'],$key); $property=$legacy; } } if (!$property) continue; $setting=$property['value']; $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); } $value=json_decode(substr($raw,$default['start'],$default['end']-$default['start']),false,512,JSON_THROW_ON_ERROR); if (!(is_string($value)||is_int($value))) throw new RuntimeException('Некорректный default UTM: '.$key); $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 (!$replacements) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); } usort($replacements,static fn(array $a,array $b): int=>$b['start']<=>$a['start']); foreach ($replacements as $replacement) $raw=substr_replace($raw,$replacement['text'],$replacement['start'],$replacement['end']-$replacement['start']); 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); }