#!/usr/bin/python
# Copyright (c) 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""This file contains helpers for representing, manipulating, and writing
OpenSSL configuration files [1]

Configuration files are simply a collection of name=value "properties", which
are grouped into "sections".

[1] https://www.openssl.org/docs/manmaster/apps/config.html
"""

class Property(object):
  """Represents a key/value pair in OpenSSL .cnf files.

  Names and values are not quoted in any way, so callers need to pass the text
  exactly as it should be written to the file (leading and trailing whitespace
  doesn't matter).

  For instance:
      baseConstraints = critical, CA:false

  Could be represented by a Property where:
    name = 'baseConstraints'
    value = 'critical, CA:false'
  """
  def __init__(self, name, value):
    self.name = name
    self.value = value


  def write_to(self, out):
    """Outputs this property to .cnf file."""
    out.write('%s = %s\n' % (self.name, self.value))


class Section(object):
  """Represents a section in OpenSSL. For instance:
      [CA_root]
      preserve = true

  Could be represented by a Section where:
    name = 'CA_root'
    properties = [Property('preserve', 'true')]
  """
  def __init__(self, name):
    self.name = name
    self.properties = []


  def ensure_property_name_not_duplicated(self, name):
    """Raises an exception of there is more than 1 property named |name|."""
    count = 0
    for prop in self.properties:
      if prop.name == name:
        count += 1
    if count > 1:
      raise Exception('Duplicate property: %s' % (name))


  def set_property(self, name, value):
    """Replaces, adds, or removes a Property from the Section:

      - If |value| is None, then this is equivalent to calling
        remove_property(name)
      - If there is an existing property matching |name| then its value is
        replaced with |value|
      - If there are no properties matching |name| then a new one is added at
        the end of the section

    It is expected that there is AT MOST 1 property with the given name. If
    that is not the case then this function will raise an error."""

    if value is None:
      self.remove_property(name)
      return

    self.ensure_property_name_not_duplicated(name)

    for prop in self.properties:
      if prop.name == name:
        prop.value = value
        return

    self.add_property(name, value)


  def add_property(self, name, value):
    """Adds a property (allows duplicates)"""
    self.properties.append(Property(name, value))


  def remove_property(self, name):
    """Removes the property with the indicated name, if it exists.

    It is expected that there is AT MOST 1 property with the given name. If
    that is not the case then this function will raise an error."""
    self.ensure_property_name_not_duplicated(name)

    for i in range(len(self.properties)):
      if self.properties[i].name == name:
        self.properties.pop(i)
        return


  def clear_properties(self):
    """Removes all configured properties."""
    self.properties = []


  def write_to(self, out):
    """Outputs the section in the format used by .cnf files"""
    out.write('[%s]\n' % (self.name))
    for prop in self.properties:
      prop.write_to(out)
    out.write('\n')


class Config(object):
  """Represents a .cnf (configuration) file in OpenSSL"""
  def __init__(self):
    self.sections = []


  def get_section(self, name):
    """Gets or creates a section with the given name."""
    for section in self.sections:
      if section.name == name:
        return section
    new_section = Section(name)
    self.sections.append(new_section)
    return new_section


  def write_to_file(self, path):
    """Outputs the Config to a .cnf files."""
    with open(path, 'w') as out:
      for section in self.sections:
        section.write_to(out)
