Merge branch 'couchdb-3293'

Closes #226
diff --git a/src/couch_doc.erl b/src/couch_doc.erl
index af14038..a913eee 100644
--- a/src/couch_doc.erl
+++ b/src/couch_doc.erl
@@ -174,6 +174,14 @@
 validate_docid(<<"_local/">>) ->
     throw({illegal_docid, <<"Illegal document id `_local/`">>});
 validate_docid(Id) when is_binary(Id) ->
+    MaxLen = case config:get("couchdb", "max_document_id_length", "infinity") of
+        "infinity" -> infinity;
+        IntegerVal -> list_to_integer(IntegerVal)
+    end,
+    case MaxLen > 0 andalso byte_size(Id) > MaxLen of
+        true -> throw({illegal_docid, <<"Document id is too long">>});
+        false -> ok
+    end,
     case couch_util:validate_utf8(Id) of
         false -> throw({illegal_docid, <<"Document id must be valid UTF-8">>});
         true -> ok
diff --git a/test/couch_doc_json_tests.erl b/test/couch_doc_json_tests.erl
index ae4d73c..9003d06 100644
--- a/test/couch_doc_json_tests.erl
+++ b/test/couch_doc_json_tests.erl
@@ -18,11 +18,13 @@
 
 setup() ->
     mock(couch_log),
+    mock(config),
     mock(couch_db_plugin),
     ok.
 
 teardown(_) ->
     meck:unload(couch_log),
+    meck:unload(config),
     meck:unload(couch_db_plugin),
     ok.
 
@@ -33,6 +35,11 @@
 mock(couch_log) ->
     ok = meck:new(couch_log, [passthrough]),
     ok = meck:expect(couch_log, debug, fun(_, _) -> ok end),
+    ok;
+mock(config) ->
+    meck:new(config, [passthrough]),
+    meck:expect(config, get, fun(_, _) -> undefined end),
+    meck:expect(config, get, fun(_, _, Default) -> Default end),
     ok.
 
 
diff --git a/test/couch_doc_tests.erl b/test/couch_doc_tests.erl
index fce4ff7..d24cd67 100644
--- a/test/couch_doc_tests.erl
+++ b/test/couch_doc_tests.erl
@@ -29,8 +29,10 @@
     ContentType = "multipart/related;boundary=multipart_related_boundary~~~~~~~~~~~~~~~~~~~~",
     DataFun = fun() -> request(start) end,
 
+    mock_config_max_document_id_length(),
     {ok, #doc{id = <<"doc0">>, atts = [_]}, _Fun, _Parser} =
         couch_doc:doc_from_multi_part_stream(ContentType, DataFun),
+    meck:unload(config),
     ok.
 
 doc_to_multi_part_stream_test() ->
@@ -75,16 +77,19 @@
 validate_docid_test_() ->
     {setup,
         fun() ->
+            mock_config_max_document_id_length(),
             ok = meck:new(couch_db_plugin, [passthrough]),
             meck:expect(couch_db_plugin, validate_docid, fun(_) -> false end)
         end,
         fun(_) ->
+            meck:unload(config),
             meck:unload(couch_db_plugin)
         end,
         [
             ?_assertEqual(ok, couch_doc:validate_docid(<<"idx">>)),
             ?_assertEqual(ok, couch_doc:validate_docid(<<"_design/idx">>)),
             ?_assertEqual(ok, couch_doc:validate_docid(<<"_local/idx">>)),
+            ?_assertEqual(ok, couch_doc:validate_docid(large_id(1024))),
             ?_assertThrow({illegal_docid, _},
                 couch_doc:validate_docid(<<>>)),
             ?_assertThrow({illegal_docid, _},
@@ -96,10 +101,15 @@
             ?_assertThrow({illegal_docid, _},
                 couch_doc:validate_docid(<<"_design/">>)),
             ?_assertThrow({illegal_docid, _},
-                couch_doc:validate_docid(<<"_local/">>))
+                couch_doc:validate_docid(<<"_local/">>)),
+            ?_assertThrow({illegal_docid, _},
+                couch_doc:validate_docid(large_id(1025)))
         ]
     }.
 
+large_id(N) ->
+    << <<"x">> || _ <- lists:seq(1, N) >>.
+
 request(start) ->
     {ok, Doc} = file:read_file(?REQUEST_FIXTURE),
     {Doc, fun() -> request(stop) end};
@@ -116,3 +126,11 @@
 collected() ->
     B = binary:replace(iolist_to_binary(get(data)), <<"\r\n">>, <<0>>, [global]),
     binary:split(B, [<<0>>], [global]).
+
+mock_config_max_document_id_length() ->
+    ok = meck:new(config, [passthrough]),
+    meck:expect(config, get,
+        fun("couchdb", "max_document_id_length", "infinity") -> "1024";
+            (Key, Val, Default) -> meck:passthrough([Key, Val, Default])
+        end
+    ).