Repository: ruby/psych (Ruby)

Issue:
Parsing aliases `aliases: false` should not raise `Psych::BadAlias`
Simple minimal example:


```ruby
#!/usr/bin/ruby

require "yaml"

yaml_with_aliases = <<~YAML
---
a: &ABC
  k1: v1
  k2: v2
b: *ABC
YAML

p YAML.load(yaml_with_aliases)
```

This raises an error: `Unknown alias: ABC (Psych::BadAlias)`

Raising an error in this case sense, but I think `Psych::BadAlias` is an incorrect error type. It communicates an incorrect impression that the issue is with the structure of the YAML, not the configuration of the `YAML`/`Psych`. The message makes it even worse.

Perhaps there should be a more specific error type, like `Psych::AliasParsingDisabled`. The message could then mention that if you want anchors/aliases to work, that you should passes `aliases: true`.

Thoughts?




Discussion:
Adding a specific exception for this case makes sense.  Can you submit a patch for it?
Sure! I just wanted to make sure it's a desirable behaviour before implementing it.

I'll probably take a stab at it some time next week.



Patch:
diff --git a/lib/psych.rb b/lib/psych.rb
index 42d79efb..4a2ab585 100644
--- a/lib/psych.rb
+++ b/lib/psych.rb
@@ -307,7 +307,7 @@ def self.unsafe_load yaml, filename: nil, fallback: false, symbolize_names: fals
   # A Psych::DisallowedClass exception will be raised if the yaml contains a
   # class that isn't in the +permitted_classes+ list.
   #
-  # A Psych::BadAlias exception will be raised if the yaml contains aliases
+  # A Psych::AliasesNotEnabled exception will be raised if the yaml contains aliases
   # but the +aliases+ keyword argument is set to false.
   #
   # +filename+ will be used in the exception message if any exception is raised
diff --git a/lib/psych/exception.rb b/lib/psych/exception.rb
index f473b95a..04a9a906 100644
--- a/lib/psych/exception.rb
+++ b/lib/psych/exception.rb
@@ -6,6 +6,13 @@ class Exception < RuntimeError
   class BadAlias < Exception
   end
 
+  # Subclasses `BadAlias` for backwards compatibility
+  class AliasesNotEnabled < BadAlias
+    def initialize
+      super "Alias parsing was not enabled. To enable it, pass `aliases: true` to `Psych::load` or `Psych::safe_load`."
+    end
+  end
+
   class DisallowedClass < Exception
     def initialize action, klass_name
       super "Tried to #{action} unspecified class: #{klass_name}"
diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb
index 935bc74f..0bf5198c 100644
--- a/lib/psych/visitors/to_ruby.rb
+++ b/lib/psych/visitors/to_ruby.rb
@@ -427,7 +427,7 @@ def resolve_class klassname
 
     class NoAliasRuby < ToRuby
       def visit_Psych_Nodes_Alias o
-        raise BadAlias, "Unknown alias: #{o.anchor}"
+        raise AliasesNotEnabled
       end
     end
   end


Test patch:
diff --git a/test/psych/helper.rb b/test/psych/helper.rb
index 0643139d..4e82887c 100644
--- a/test/psych/helper.rb
+++ b/test/psych/helper.rb
@@ -51,7 +51,7 @@ def assert_to_yaml( obj, yaml, loader = :load )
           :UseVersion => true, :UseHeader => true, :SortKeys => true
         )
       ))
-    rescue Psych::DisallowedClass, Psych::BadAlias
+    rescue Psych::DisallowedClass, Psych::BadAlias, Psych::AliasesNotEnabled
       assert_to_yaml obj, yaml, :unsafe_load
     end
 
@@ -61,7 +61,7 @@ def assert_to_yaml( obj, yaml, loader = :load )
     def assert_parse_only( obj, yaml )
       begin
         assert_equal obj, Psych::load( yaml )
-      rescue Psych::DisallowedClass, Psych::BadAlias
+      rescue Psych::DisallowedClass, Psych::BadAlias, Psych::AliasesNotEnabled
         assert_equal obj, Psych::unsafe_load( yaml )
       end
       assert_equal obj, Psych::parse( yaml ).transform
@@ -79,7 +79,7 @@ def assert_cycle( obj )
           assert_equal(obj, Psych.load(v.tree.yaml))
           assert_equal(obj, Psych::load(Psych.dump(obj)))
           assert_equal(obj, Psych::load(obj.to_yaml))
-        rescue Psych::DisallowedClass, Psych::BadAlias
+        rescue Psych::DisallowedClass, Psych::BadAlias, Psych::AliasesNotEnabled
           assert_equal(obj, Psych.unsafe_load(v.tree.yaml))
           assert_equal(obj, Psych::unsafe_load(Psych.dump(obj)))
           assert_equal(obj, Psych::unsafe_load(obj.to_yaml))
diff --git a/test/psych/test_array.rb b/test/psych/test_array.rb
index 28b76da7..0dc82439 100644
--- a/test/psych/test_array.rb
+++ b/test/psych/test_array.rb
@@ -57,6 +57,22 @@ def test_self_referential
       assert_cycle(@list)
     end
 
+    def test_recursive_array
+      @list << @list
+
+      loaded = Psych.load(Psych.dump(@list), aliases: true)
+
+      assert_same loaded, loaded.last
+    end
+
+    def test_recursive_array_uses_alias
+      @list << @list
+
+      assert_raise(AliasesNotEnabled) do
+        Psych.load(Psych.dump(@list), aliases: false)
+      end
+    end
+
     def test_cycle
       assert_cycle(@list)
     end
diff --git a/test/psych/test_hash.rb b/test/psych/test_hash.rb
index 53747813..0efa2116 100644
--- a/test/psych/test_hash.rb
+++ b/test/psych/test_hash.rb
@@ -112,6 +112,24 @@ def test_ref_append
       assert_equal({"foo"=>{"hello"=>"world"}, "bar"=>{"hello"=>"world"}}, hash)
     end
 
+    def test_recursive_hash
+      h = { }
+      h["recursive_reference"] = h
+
+      loaded = Psych.load(Psych.dump(h), aliases: true)
+
+      assert_same loaded, loaded.fetch("recursive_reference")
+    end
+
+    def test_recursive_hash_uses_alias
+      h = { }
+      h["recursive_reference"] = h
+
+      assert_raise(AliasesNotEnabled) do
+        Psych.load(Psych.dump(h), aliases: false)
+      end
+    end
+
     def test_key_deduplication
       unless String.method_defined?(:-@) && (-("a" * 20)).equal?((-("a" * 20)))
         pend "This Ruby implementation doesn't support string deduplication"
diff --git a/test/psych/test_merge_keys.rb b/test/psych/test_merge_keys.rb
index dcf4f1fc..8d2fceab 100644
--- a/test/psych/test_merge_keys.rb
+++ b/test/psych/test_merge_keys.rb
@@ -117,7 +117,7 @@ def test_missing_merge_key
 bar:
   << : *foo
       eoyml
-      exp = assert_raise(Psych::BadAlias) { Psych.load yaml }
+      exp = assert_raise(Psych::BadAlias) { Psych.load(yaml, aliases: true) }
       assert_match 'foo', exp.message
     end
 
diff --git a/test/psych/test_object.rb b/test/psych/test_object.rb
index 0faf6b24..21c27794 100644
--- a/test/psych/test_object.rb
+++ b/test/psych/test_object.rb
@@ -36,10 +36,19 @@ def test_tag_round_trip
     def test_cyclic_references
       foo = Foo.new(nil)
       foo.parent = foo
-      loaded = Psych.unsafe_load Psych.dump foo
+      loaded = Psych.load(Psych.dump(foo), permitted_classes: [Foo], aliases: true)
 
       assert_instance_of(Foo, loaded)
-      assert_equal loaded, loaded.parent
+      assert_same loaded, loaded.parent
+    end
+
+    def test_cyclic_reference_uses_alias
+      foo = Foo.new(nil)
+      foo.parent = foo
+
+      assert_raise(AliasesNotEnabled) do
+        Psych.load(Psych.dump(foo), permitted_classes: [Foo], aliases: false)
+      end
     end
   end
 end
diff --git a/test/psych/test_safe_load.rb b/test/psych/test_safe_load.rb
index b52d6048..a9ed7375 100644
--- a/test/psych/test_safe_load.rb
+++ b/test/psych/test_safe_load.rb
@@ -19,18 +19,31 @@ class Foo; end
       end
     end
 
-    def test_no_recursion
-      x = []
-      x << x
-      assert_raise(Psych::BadAlias) do
-        Psych.safe_load Psych.dump(x)
+    def test_raises_when_alias_found_if_alias_parsing_not_enabled
+      yaml_with_aliases = <<~YAML
+        ---
+        a: &ABC
+          k1: v1
+          k2: v2
+        b: *ABC
+      YAML
+
+      assert_raise(Psych::AliasesNotEnabled) do
+        Psych.safe_load(yaml_with_aliases)
       end
     end
 
-    def test_explicit_recursion
-      x = []
-      x << x
-      assert_equal(x, Psych.safe_load(Psych.dump(x), permitted_classes: [], permitted_symbols: [], aliases: true))
+    def test_aliases_are_parsed_when_alias_parsing_is_enabled
+      yaml_with_aliases = <<~YAML
+        ---
+        a: &ABC
+          k1: v1
+          k2: v2
+        b: *ABC
+      YAML
+
+      result = Psych.safe_load(yaml_with_aliases, aliases: true)
+      assert_same result.fetch("a"), result.fetch("b")
     end
 
     def test_permitted_symbol